Repository navigation
Keep fee quotes tied to the filing they priced - #249
Conversation
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
left a comment
There was a problem hiding this comment.
The fingerprint approach looks solid overall, but there is one concurrency hole that can mark an old fee response as current.
| recorded = False | ||
| if draft is not None and quote is not None: | ||
| total, breakdown = quote | ||
| record_fee_quote( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
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>
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.pyfingerprints every input the EFSP fee calculation reads:The quote stores the fingerprint it was priced on (new field
quoted_fee_fingerprint, migration0026).fee_quote_state()returnscurrent,stale,missing, orwaived. 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
/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 includesquote_recordedand aquotesummary. If the EFSP answers without a readable total, nothing is recorded and Payment says so.quoted_fee_totalorquoted_fee_breakdownfrom the form. The hidden inputs are removed.review.jsrequests fees from Review with the same payload it would submit, and shows the new total. Submit stays disabled until then.submit_final_filingrefuses 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.jshandles 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:review.jsin Chromium against the real Review page with the fee API mocked in the browser:"0.00".pytest -n autopasses (880), JS unit tests pass (43), pre-commit (includingty) passes.Note for #232
invalidate_fee_quote()andfee_quote_state()are the pieces the new/existing path change should use.🤖 Generated with Claude Code