Skip to content

fix(wallet): retain card form ownership through dismissal - #753

Merged
NewWorldVenture merged 4 commits into
mainfrom
jimmy/money-card-form-ownership-20261001
Oct 1, 2026
Merged

NewWorldVenture merged 4 commits into
mainfrom
jimmy/money-card-form-ownership-20261001

Conversation

@NewWorldVenture

Copy link
Copy Markdown
Owner

Physical-card ordering and card-control saving could leave a disabled spinner after a rejected action, submit twice before a render or after reopening, and let an older successful request close a newer form with unsaved input. Pending ownership now survives child dismissal in the mounted card view. Each opening has an identity, so stale responses cannot close or toast into a newer interaction; completed successful mutations still refresh the mounted card data.

Only money-cards-view.tsx and a paired mounted browser spec change in this follow-on. It stacks on #751 and retains its separate guard correction by normal merge. Actions, authorization, provider helpers, catalogues and database code are unchanged. Closing a form does not cancel a dispatched mutation; no automatic retry or cross-tab/reload idempotency is claimed.

Validation:

  • Before repair: 24 desired browser assertions failed; 9 ordinary controls passed.
  • After repair: all 33 new mounted cases and all 20 existing fix(wallet): preserve pending ownership for card actions #751 cases passed, with zero network requests and mocked action responses.
  • Independent React 19 StrictMode review: 16 additional mounted probes passed.
  • Ten in-memory source mutations were detected; source bytes remained unchanged.
  • Full TypeScript check, targeted lint and diff checks passed.
  • After normal-merging the fix(wallet): preserve pending ownership for card actions #751 guard correction, all 53 mounted tests, 196 relevant unit/guard tests, and the full TypeScript check passed again.
  • Related unit run: 170 passed; the sole failure is an unchanged Windows shell-quoting scanner (a-write-result-is-never-discarded.test.ts), independently reproduced on unmodified fix(wallet): preserve pending ownership for card actions #751 source. The failure receipt is retained; no scanner guard was weakened.

Audit scope: COMPONENT-CF064547E2DE / PAGE-346 / UI-ROUTE-0343. This is synthetic UI/action-boundary verification, not live provider or financial workflow acceptance. Root retains the final audit ledger.

@vercel

vercel Bot commented Oct 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 1, 2026 6:53am UTC

Request Review

@NewWorldVenture NewWorldVenture left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Independent current-main composition gate passed (2026-10-01 06:57 UTC).

Base: main 3dd5a7b5a407c7223ff3730de905a5f41d9fdb85. Composed exact heads #749 62a4a918, #751 07767475, #752 8bf35567, #753 3a564452, and ledger #750 3f52287c through normal merges. Local aggregate head a8397a3a90a496afed2aa5b5da04943ca81db7ab, tree 92cc33bba22e612f574112e8c3ded1d0281851ba.

  • 1,158 unit/document checks across 29 files passed.
  • 69 mounted Chromium/React 19 cases passed: 16 reveal + 20 pending-operation + 33 form cases; zero retries, skips, or flakes.
  • Full TypeScript and changed-source lint passed.
  • Independent source review verified exactly nine expected paths against the authorized PR blobs; all 5,546 other tracked entries match main, including authentication, migrations, catalogues, actions, and dependencies. All source-head ancestry retained.

The separate mounted suites use synthetic actions/provider SDKs; this is not live provider/iframe, server idempotency, cancellation of already-dispatched mutations, cross-tab/reload, or full Money workflow acceptance. Exact candidate hosted checks and post-integration CI/served SHA remain required. Main stays stable while its current post-merge E2E finishes.

Durable local evidence: money-ui-combined-0654-evidence/ (unit/browser/types/lint receipts and independent-source-review.json, SHA256 1db10d78ab1e7fd29ca5ea8beae59f4085326f43d7b06ef61798392fdf4bc212).

@NewWorldVenture
NewWorldVenture marked this pull request as ready for review October 1, 2026 07:34
@NewWorldVenture
NewWorldVenture merged commit f721c0a into main Oct 1, 2026
7 checks passed
NewWorldVenture pushed a commit that referenced this pull request Oct 2, 2026
…rited claims

Self-review of #811 (repair A and C), findings verified before fixing:

- The owner was keyed on the client session (useAuthenticatedCacheScope).
  That identity settles in place (pending -> ready on the first agreeing
  bootstrap, no remount) and drops to 'unavailable' with a remount, so a
  claim filed under one owner was invisible after the switch and the
  control re-enabled while its request was still running. The owner is
  now the actor and family the page was rendered for (useApp().userId,
  familyId), fixed for the life of a mount; another account or family
  remounts the app with its own identity. The same actor signing in
  again sees their own in-flight order, which is still theirs.
- A remounted view released an inherited physical-order or controls
  claim with no reconciliation (the unmounted form presents nothing,
  per #753), re-enabling Order over a stale list. The mounted view now
  re-reads (router.refresh) when a claim it did not make settles; its
  own claims are reconciled by their handlers as before. The unmounted
  instance's behaviour is unchanged.
- Spec: forced dispatch used click() on a disabled button, which never
  reaches the handler; it now calls the control's React handler (as the
  existing specs do), so the held-key check itself is exercised.
- ensureCardholder logs a failed or rejected duplicate re-read (still
  refusing), so an outage is not read as a uniqueness conflict.
- The concurrency test's fallback timer is cleared, and both cases
  assert the barrier opened on both arrivals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
NewWorldVenture added a commit that referenced this pull request Oct 4, 2026
… adopt only the exact duplicate cardholder (repairs A + C) (#811)

* fix(cards): adopt only the exact duplicate cardholder a concurrent first order inserted

JIMMY-SUPPORT-CARD-RETRY-20261001, repair C (owner review 5378576349).

Two first orders for one child (Virtual and the physical dialog claim
separately) both miss the cardholder lookup. The stable idempotency key
gives both the same provider cardholder, so the second insert hit
UNIQUE (family_id, member_id) and that order was refused, reported as
"That already exists", with no card issued.

On a 23505 only, ensureCardholder now re-reads (family_id, member_id)
and adopts that row when its family, member and provider cardholder id
all equal this call's. Any other outcome keeps the original refusal:
a different provider cardholder, another family or member, no row, a
returned re-read error (even beside exact data), a rejected re-read,
and every non-23505 insert failure (which is not re-read at all).

Tests (tests/stripe-cardholder-lookup.test.ts, sealed harness): exact
adoption with the scoped re-read; five fail-closed re-read cases; a
rejected re-read; three non-duplicate errors without re-read; and two
concurrent first orders against the in-memory unique constraints (one
row, both resolve; a different provider cardholder still refused).

No schema, migration, provider call or business rule change.

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

* fix(cards): keep a pending card operation pending across a remount, per signed-in owner

JIMMY-SUPPORT-CARD-RETRY-20261001, repair A (owner review 5378576349).

MoneyCardsView held its pending claims in a per-instance ref. Leaving
the page and returning before a request answered gave a fresh view with
no claims and the same stale props, so a second freeze or a second card
order for the same child went out while the first was in flight.

Claims now live in a module registry keyed by owner: the signed-in user,
that sign-in's session and the active family, from the existing
useAuthenticatedCacheScope() partition. The same owner returning sees
the operation still pending; another account, a new sign-in, another
family, or an identity that is not settled ('pending', 'blocked',
'unavailable') never does. Unsettled views get a private per-mount
owner, which is the previous behaviour. A claim is released under the
owner that made it when its request settles, mounted or not, and only
once. Completion of an already committed action (toast, refresh) is
unchanged; the post-unmount announcement stays an open UX decision.

Independent claim keys are unchanged, so Virtual and Physical for one
child, and repeat orders after a settled one, still go out.

Tests: new tests/e2e/money-cards-pending-owner.spec.ts (13 cases:
remount for order and freeze, five identity changes, unsettled views,
in-place identity change, completion while unmounted x3, virtual plus
physical). The two existing view specs mock the new import with no
settled identity, keeping their per-view semantics.

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

* test(cards): reset queued lookup answers per duplicate-insert case

A case whose code path skips the re-read must not leave a queued answer
for the next case; vi.clearAllMocks() does not clear once-values.

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

* fix(cards): own pending card work by actor and family; reconcile inherited claims

Self-review of #811 (repair A and C), findings verified before fixing:

- The owner was keyed on the client session (useAuthenticatedCacheScope).
  That identity settles in place (pending -> ready on the first agreeing
  bootstrap, no remount) and drops to 'unavailable' with a remount, so a
  claim filed under one owner was invisible after the switch and the
  control re-enabled while its request was still running. The owner is
  now the actor and family the page was rendered for (useApp().userId,
  familyId), fixed for the life of a mount; another account or family
  remounts the app with its own identity. The same actor signing in
  again sees their own in-flight order, which is still theirs.
- A remounted view released an inherited physical-order or controls
  claim with no reconciliation (the unmounted form presents nothing,
  per #753), re-enabling Order over a stale list. The mounted view now
  re-reads (router.refresh) when a claim it did not make settles; its
  own claims are reconciled by their handlers as before. The unmounted
  instance's behaviour is unchanged.
- Spec: forced dispatch used click() on a disabled button, which never
  reaches the handler; it now calls the control's React handler (as the
  existing specs do), so the held-key check itself is exercised.
- ensureCardholder logs a failed or rejected duplicate re-read (still
  refusing), so an outage is not read as a uniqueness conflict.
- The concurrency test's fallback timer is cleared, and both cases
  assert the barrier opened on both arrivals.

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

* test(cards): R5 after repair C — one cardholder, the same-millisecond card key refuses one

With ensureCardholder adopting the exact duplicate (repair C), two first
orders for one child no longer fail at the cardholder step. They now meet
the inherited card-<wallet>-<ms> idempotency key, so in one millisecond
the provider refuses the second with the action's own text. Same verified
patch as #811 comment 5968508930.

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

---------

Co-authored-by: Claude <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.

1 participant