Skip to content

Keep fee quotes tied to the filing they priced - #249

Merged
nonprofittechy merged 2 commits into
mainfrom
fix/193-invalidate-stale-fee-quotes
Sep 29, 2026
Merged

nonprofittechy merged 2 commits into
mainfrom
fix/193-invalidate-stale-fee-quotes

Conversation

@nonprofittechy

Copy link
Copy Markdown
Member

Closes #193.

Problem

Payment saved the quoted total and breakdown on the draft, and Review displayed them. Nothing linked the quote to the inputs it was priced on. The only clearing happened in the clerk-correction handoff paths. After ordinary edits, such as the court, case type, a document's filing type, or an optional service, Review still showed the old total, and the filing could be submitted against it.

Approach: a fingerprint instead of per-screen clearing

Fee-affecting data can be edited from half a dozen screens, so this doesn't add "remember to clear the quote" to each one. The new efile/services/fee_quotes.py fingerprints every input the EFSP fee calculation reads:

  • court, case category and case type
  • new or existing case, and the existing case's id
  • amount in controversy
  • draft-level optional services
  • the payment account
  • for each document: role, filing type, document type, component, and requested optional services
  • each party's role, party type, filing-for flag, and whether it is an organization

The quote stores the fingerprint it was priced on (new field quoted_fee_fingerprint, migration 0026). fee_quote_state() returns current, stale, missing, or waived. The list is deliberately generous: an input that turns out not to affect the fee costs one extra fee request, while one left out would let a stale total through. Quotes saved before this change have no fingerprint, so they count as stale and get requoted.

Behavior

  • Fee API (/api/payment-fees/): when the EFSP answers, it records the total and breakdown on the draft with the fingerprint, for the payment account it was asked about. The response includes quote_recorded and a quote summary. If the EFSP answers without a readable total, nothing is recorded and Payment says so.
  • Payment: the POST no longer takes quoted_fee_total or quoted_fee_breakdown from the form. The hidden inputs are removed.
  • Review:
    • A total is shown only while it is current.
    • A stale or missing quote shows "Something that affects the court's fees changed since they were calculated. We are calculating them again." Then review.js requests fees from Review with the same payload it would submit, and shows the new total. Submit stays disabled until then.
    • If the requote fails, the page says the fees could not be determined, links to Payment, and keeps Submit disabled.
  • Submission: submit_final_filing refuses with a 412 (submission_fee_quote_stale) before claiming the draft, unless the quote is current or the account is a fee waiver. If something changed in another tab, review.js handles the 412 by requoting.
  • invalidate_fee_quote() is the single way to drop a quote outright. The document-replacement handoff now uses it. The correction-draft copy also clears the fingerprint.

Tests

  • efile/tests/test_fee_quotes.py, 27 tests:
    • each fee-affecting edit makes the quote stale (13 cases)
    • edits that don't price the filing leave it current
    • quotes saved before this change count as stale, and waivers need no quote
    • the fee API records the quote, and an answer without a total is not recorded
    • Review doesn't show a stale total
    • submission is refused without a current quote and is allowed for a waiver
    • the regression the issue asks for: remove an optional service through Organize documents from Review. Review goes stale and submission is refused. The fee request Review makes brings the quote current and submission goes through, all without visiting Payment.
  • Existing submission tests now record a current quote. The end-to-end flow uses an actual waiver account type. The Payment and Review fee tests were updated for the new contract.
  • The optional-services Playwright E2E no longer revisits Payment by hand. After removing the service it stays on Review and waits for the automatic requote ($256.25, no optional-service line). I did not run it here because it needs the Tyler test account.
  • I checked review.js in Chromium against the real Review page with the fee API mocked in the browser:
    • a current quote makes no fee request
    • a stale quote shows the pending text with Submit disabled, then the new total with Submit enabled
    • a failed requote shows the error and keeps Submit disabled
  • The accessibility seed now records a real current quote instead of a bare "0.00".
  • Results: pytest -n auto passes (880), JS unit tests pass (43), pre-commit (including ty) passes.

Note for #232

invalidate_fee_quote() and fee_quote_state() are the pieces the new/existing path change should use.

🤖 Generated with Claude Code

A fee quote was saved on Payment and shown on Review, but nothing tied
it to what it priced. Changing the court, case type, a filing type or an
optional service afterwards left the old total on Review and let the
filing be submitted against it (#193).

The quote now carries a fingerprint of every fee-bearing input: court,
category, type, new/existing case and case id, amount in controversy,
payment account, each document's filing type, component and optional
services, and the parties' roles. It is current only while the draft
still produces the same fingerprint, so any edit on any screen makes it
stale without that screen having to remember to clear it.

- The fee API records the quote on the draft when the EFSP answers,
  priced for the account it was asked for; Payment no longer takes a
  total from its form.
- Review shows a total only while it is current. A stale or missing
  quote is requoted from Review itself, with Submit held until the new
  total is on screen; a failed requote says fees could not be
  determined and keeps Submit off.
- Submission is refused (412, before the draft is claimed) without a
  current quote or a fee waiver.
- invalidate_fee_quote() is the one way to drop a quote outright, used
  by the document-replacement handoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@nonprofittechy nonprofittechy left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The fingerprint approach looks solid overall, but there is one concurrency hole that can mark an old fee response as current.

Comment thread efile_app/efile/api/filing_views.py Outdated
recorded = False
if draft is not None and quote is not None:
total, breakdown = quote
record_fee_quote(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The EFSP response here priced the efile_data captured before the network call, but record_fee_quote() computes its fingerprint from the draft after that call returns. If another tab changes a fee-affecting field while the (up to 60s) fee request is in flight, the old total can be stored with the new draft’s fingerprint and fee_quote_state() will report CURRENT—defeating the stale-quote/submission guard this PR adds. Could you snapshot the expected fingerprint before sending the fee request and only record the response if the current draft still matches it (or otherwise bind the stored fingerprint to the exact inputs/payload that was priced)? A test that mutates the draft inside the mocked requests.post would catch this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e342a77. This binds the quote to the draft state the payload was built from, not just to the state at the start of the request. Payment and Review now render a token of the draft's fee inputs (the fingerprint without the payment account, which is bound separately). The page sends that token with the fee request. The quote is recorded only if the draft still matches the token before the EFSP call and again, under a row lock, when it answers. If it doesn't match, the response has quote_superseded: true, nothing is stored, and the page asks the filer to reload. New tests: the draft changes inside the mocked requests.post, the request comes from an older page, and the request carries no token. The optional-services regression now takes its token from the Review page itself.

The EFSP priced the payload the page built, but the quote was stored
with a fingerprint of the draft as it was when the answer came back. An
edit in another tab during the (up to a minute) fee request would leave
the old total recorded as current (review on #249).

Payment and Review now carry a token of the draft's fee inputs as they
drew it, and send it with the fee request. The quote is recorded only if
the draft still gives that token before the EFSP is asked and again,
under a row lock, when it answers; otherwise the response says
quote_superseded and the page asks the filer to reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nonprofittechy
nonprofittechy merged commit dcbc678 into main Sep 29, 2026
7 checks passed
@nonprofittechy
nonprofittechy deleted the fix/193-invalidate-stale-fee-quotes branch September 29, 2026 02:46
nonprofittechy added a commit that referenced this pull request Sep 29, 2026
Found reviewing the four together:

- A path change drops the fee quote with invalidate_fee_quote(), so the
  fingerprint #249 added goes with the total.
- The Tyler E2E helpers: selectGuidedCourt answers a short list as
  radios (#248), reopens a question already folded and presses Update
  court when changing a settled court; chooseFilingPath goes through
  Change when the new-or-existing answer is shown as a summary (this
  PR). The matrix and optional-services specs use it.
- People: the party list Save role lands on no longer draws a focus
  ring (#247).
- The walkthrough says what Save role and Continue do, and that Review
  recalculates fees after a fee-affecting change.
- ty warnings in the new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Invalidate and recalculate filing fee quotes after fee-affecting edits

1 participant