Repository navigation
fix(cards): refuse a stale card order and recover a committed card whose reply was lost (follow-up to #882) - #903
Closed
NewWorldVenture wants to merge 10 commits into
Conversation
… (failing) A tab whose view is stale (it still shows N cards of a type for a child) orders that type after another tab's card was mirrored. The mirrored count is N+1 by then, so repair B's attempt key treats it as a new attempt and the provider issues a second live card; nothing in the request tells it from a deliberate order. New sealed cases drive the real issueCardAction -> ensureCardholder -> issueCard against the shared synthetic provider and the in-memory mirror, with the count the view showed sent as expectedCount: - a stale order (expectedCount N while N+1 are mirrored, N = 0 and 2, both types) must be refused with wallet.refreshToTryAgain: no provider create, no row, no audit; - an overlapping order whose count read lands after the first card was mirrored is refused the same way; one held at the provider response still shares B's key and gets the one card (control, passes today); - an order ahead of the mirror is refused too; every status counts; virtual, physical and each child are counted on their own; - an expectedCount that is not a non-negative safe integer (negative, fractional, NaN, infinite, beyond the safe integers, a string, null, missing) is refused before any read, Trust check or provider call; - controls passing today: a deliberate order against the up-to-date count issues a new card; an unreadable count is still the generic refusal. On the current source 20 of 28 fail (the stale and validation cases); the 8 controls pass. The file does not typecheck until issueCardAction accepts expectedCount. Test-only. Audit JIMMY-SUPPORT-CARD-RETRY-20261001. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
…moved past Repair B keys a card order by the mirrored count of the child's cards of that type, so overlapping requests of one order share a key. A stale tab that still shows N cards and orders after another tab's card was mirrored reads N+1, takes a new key and gets a second live card: the server could not tell it from a deliberate order. The view now sends the count it showed: at the Virtual button, the issue-all loop and the physical order dialog (passed in as a prop), expectedCount is the number of that child's cards of that type in its cards prop, every status. issueCardAction refuses an expectedCount that is not a non-negative safe integer with the generic money.couldNotIssueTheCard, before the service client, Trust or provider, and passes it to issueCard. After its count read, issueCard throws StaleCardOrderError (an exported class) when the mirrored count differs, in either direction, before getStripe(); B's key is unchanged. The action maps that class, not a message, to wallet.refreshToTryAgain, without the failure log; the four locales without the key fall back to their base locale. On that answer the Virtual button and the dialog show the error toast as before and call router.refresh(), so the next order is made against what now exists; the issue-all path already refreshes after every run. The view recognises the answer by comparing it with its own translation of the same key, since the action's result type is unchanged. A parent whose view is stale but who does want another card has to refresh first. Audit JIMMY-SUPPORT-CARD-RETRY-20261001. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
…stale-order answer Only what the expectedCount input changes is rewritten; every count stays exact. - Inputs to issueCardAction carry the count the sending view showed: 0 for a first order, the up-to-date count for a deliberate later one (attempt identity, characterization, Treasury/card actions, Trust bridge). - Attempt identity: the "residual" second order after the first card is mirrored is now refused as stale when sent against the old count (one card, one key); sent against the new count it is a deliberate second card keyed by the count. Seeded-row cases send the count of this child's virtual cards the seed makes (0 or 1, or 2 and 3 for the status case). - Characterization: the remounted-view re-click (same count after the first card is mirrored) is now "repaired (stale guard)": refused, one live card. The same-millisecond case keeps its point, no clock collision, by sending the up-to-date count. The deliberate-orders control sends up-to-date counts. - Treasury/card actions: the issueCard pin includes expectedCount (3 for one case, so it is forwarded, not assumed). New cases: a StaleCardOrderError from issueCard answers wallet.refreshToTryAgain with no audit, refresh or failure log; an error merely named or worded like it stays the generic refusal; an expectedCount of -1, 0.5, NaN, Infinity, '0', null or undefined is refused before service, Trust or provider access. The issuing mock exports a stand-in class. - Browser (cross-tab): the stale-tab case runs for the Virtual button, issue-all and the physical dialog. Both tabs send expectedCount 0; the held answer to tab 2 is the stale refusal; tab 2 shows it, calls router.refresh() once, shows the card, and its next order carries 1. A new case pins per-child, per-type, every-status counts (1, 2, 1 where the child's total is 3 and the family's 4). The two "identical calls" cases now record the shared count. - Pending-owner and form-ownership specs: only their exact issueCardAction inputs gain expectedCount. Audit JIMMY-SUPPORT-CARD-RETRY-20261001. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
… read apart The %j titles printed NaN, Infinity and null all as "null". Titles only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
…tale-order guard Brings in fac3583 (issueCard names a failed provider create and a mismatched duplicate in the log), 8887c9d (cross-tab spec comments) and 208c7e2 (#876's braces allowlist and CI comment). lib/stripe/issuing.ts and tests/money-card-attempt-identity.test.ts merged cleanly: the expectedCount guard stays right after the count check and before getStripe(); the provider create keeps its .catch logging. tests/e2e/money-cards-cross-tab.spec.ts conflicted on comments only. Kept this branch's header and the rewritten stale-tab cases (the base's comment said a stale tab still gets a second card, which the guard ends), with the base's "not exercised here" sentence folded into the header; took the base's wording for the two overlapping-call comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
…words issueCardAction answers a StaleCardOrderError with stale: true beside the translated text, and the view re-reads on that flag. Matching the text tied the refresh to one locale's wording; a browser case pins that a refusal in other words still re-reads with the flag and one that only reads like it does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
…failing) The insert commits, its reply is lost, and the order answers 'could not issue' with no card_issued audit although the card is live and mirrored. A lost answer to the whole action is already safe: the unrefreshed view's retry is refused as stale. Raised in the owner's review of #882 (5970508018). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
A card mirror INSERT that reports an error other than 23505 may have committed with only its reply lost. issueCard now reads the card back by stripe_card_id after any insert error. Exactly this attempt's row after a non-duplicate error is this order's own card: it is answered and audited here, where the parent used to be told 'could not issue' for a live, mirrored, unaudited card. A 23505 still adopts another request's row without a second audit. Anything else, including a failed read-back, keeps the refusal and names the card. Raised in the owner's review of #882 (5970508018). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 3, 2026
…vtf' into claude/card-stale-order-guard-7q6vtf
This was referenced Oct 3, 2026
… refusals From the independent review of #903: after a stale refusal the physical dialog stayed open over the refreshed list, one click from a second card ordered against the new count before the parent had seen the card that made it stale. It now closes and the view re-reads. issueCard logs each stale refusal (ids and counts only) at warn level, and the error's docstring no longer claims nothing reached the provider: a first order may already have created the cardholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
This was referenced Oct 3, 2026
NewWorldVenture
marked this pull request as ready for review
October 4, 2026 14:59
Owner
Author
|
Closing again at the owner's direction to close the 85-PR backlog: this PR was in the sweep at 15:52 UTC and was reopened at 16:13 UTC. The branch is kept; nothing is merged or deleted. Please do not reopen without the owner's say-so. Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to repair B: an expected-count contract and recovery of a committed-but-unacknowledged card mirror (JIMMY-SUPPORT-CARD-RETRY-20261001). It answers the owner's independent review of #882 (5970508018).
Draft. Do not merge without coordination. Stacked on #882: base
claude/card-attempt-identity-7q6vtfat208c7e201. Claim: #811 5969137460. Theactions.tsscope isissueCardActiononly.What #882 left open
card_issuedaudit.The change
components/wallet/money-cards-view.tsx): every order (Virtual, issue-all, the physical dialog) sendsexpectedCount, the number of that child's cards of that type it shows, in every status.issueCardActionrefuses anexpectedCountthat isn't a non-negative safe integer, before any read, Trust check or provider call.issueCardthrowsStaleCardOrderErrorbefore any provider call when the mirrored count differs, in either direction.{ ok: false, error: t('wallet.refreshToTryAgain'), stale: true }. That's an existing key, so no i18n file is touched.onStale, re-read on thestaleflag, not on the words.issueCard. After any mirror insert error, it reads the card back bystripe_card_idand adopts exactly this attempt's row:Results (synthetic provider; in-memory UNIQUE)
08006)Tests
tests/money-card-stale-order.test.ts, 32 cases. Against fix(cards): one card order gives one card across tabs — attempt identity (repair B) #882'sissuing.tsandactions.ts, 24 fail; the 8 controls pass on both: overlap, count-read failures and a deliberate order.eac32892e,87a132998).08006answer.tests/money-card-attempt-identity.test.ts. fix(cards): one card order gives one card across tabs — attempt identity (repair B) #882's "does not re-read after a non-duplicate insert failure" is deliberately replaced by four groups:money-treasury-card-actionsandmoney-card-trust-bridgetests now sendexpectedCount. Every count stays exact.tests/e2e/money-cards-cross-tab.spec.ts, 10 cases):staleflag even when the text is localized, and not on a plain refusal that only reads like it;expectedCountto recorded inputs.Mutants: 13/13 killed. They cover:
Validation on
bd8543692:tsc,npm run lintandi18n:gateare clean.Independent review of
bd8543692(read-only)1494abb04:tscand lint clean.Residuals (stated, not fixed)
max_rows.Not covered: live Stripe, RLS, a served browser → server → provider workflow, or production. No schema, migration, credential or provider change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
Generated by Claude Code