Repository navigation
Money: a hold Stripe declined on our behalf is released (stacked on #954; no migration) - #962
Closed
NewWorldVenture wants to merge 1 commit into
Closed
NewWorldVenture wants to merge 1 commit into
NewWorldVenture wants to merge 1 commit into
Conversation
If Stripe does not have our decision within 2 seconds it approves or declines by the account's timeout setting, and reports that only in issuing_authorization.created (request_history.reason webhook_timeout). handleAuthorizationRequest reserves the hold first and then calls the deprecated approve API, so a slow run had committed the hold when the approve call failed; the route ignored .created and nothing released it. The child's money stayed held for good. - The route handles issuing_authorization.created through the same path as .updated. That path now also releases, by its key, the hold of every request Stripe declined, whatever the authorization's status, so a declined increase no longer waits for the close. - When our approve call fails, the handler asks Stripe what it decided: an authorization that is over is settled and closed as its own event would be; a request declined for good (matched by its position and the amount it asked for) releases that request's hold only. Approved, undecided or unknown: the hold stays. - network_fallback (Stripe declined, the card network decided) is not acted on early: when the network approved, Stripe says it may still be captured, so its hold waits for the close, which releases it. - releaseRequestHold releases one request's hold by key and amount, and confirms its write (.select), as the unconfirmed-write ratchet requires. - docs/AGENT_HANDOFF.md step 4 subscribed the money endpoint to three events and left out issuing_authorization.updated, which releases a reversed or expired hold; LB-006 listed .created, which the route did not handle. Both now list exactly HANDLED_EVENT_TYPES, pinned by a test. Found by reading; red tests first. Two independent review rounds are addressed (ratchet, network_fallback without stranding holds, terminal retrieve, request identity by amount, per-request release on .updated, error never replaced, test gaps); 12 app mutants killed. Claim: #771 5981789351. App-only: no migration. 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 4, 2026
Owner
Author
|
Closing at the owner's direction to clear every open pull request. The branch is kept; nothing is merged or deleted. Please do not reopen or open a replacement 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.
Draft for review: do not merge without the owner.
claude/card-hold-settles-7q6vtf), which is stacked on Money: a card refund is credited back to the child, and each card capture or refund posts once #925. Retarget tomainonce those land.What was wrong
A hold for a purchase Stripe declined on our behalf was never released.
issuing_authorization.created, withrequest_history.reason=webhook_timeout(Stripe: real-time authorizations).handleAuthorizationRequestreserves the hold first, then calls the deprecatedauthorizations.approveAPI. So when a slow run misses the window, the hold is already committed and the approve call fails..created, and nothing else released the hold. The child's money stayed held for good..createdalone would not be enough: it can be processed before the late hold commits.The setup docs and the code disagreed.
docs/AGENT_HANDOFF.mdstep 4 subscribes the money endpoint to.request,issuing_transaction.createdandaccount.updatedonly. It leaves outissuing_authorization.updated, which releases a reversed or expired hold (and runs Money: card holds follow each authorization request and each capture, in any event order (0487, stacked on #925) #954's settle-before-close).docs/runbooks/LB-006listed.created, which the route answered as unhandled.The change (app only)
app/api/webhooks/money/route.ts: handlesissuing_authorization.createdthrough the same path as.updated. Stripe sends.createdfor every authorization, and handling is idempotent, so both events may arrive.lib/stripe/webhook.ts,handleAuthorizationUpdated:request_history, matched by the amount it asked for. An increase never frees the purchase it raised.approve), undecided, not recognisably this request, or Stripe cannot be asked: the hold stays. Money is held, never freed.network_fallbackis not a decline we act on early. Stripe declined but the card network decided; when the network approved, Stripe's docs say it may still be captured.lib/wallet/server.ts:releaseRequestHoldreleases one request's hold by its key and the amount it asked for, so Stripe's record and our hold must agree before money moves. It confirms its write (.select('id')), as the unconfirmed-write ratchet requires.HANDLED_EVENT_TYPES, and a test pins them to the route:issuing_authorization.request,.createdand.updated;issuing_transaction.created;issuing_card.createdand.updated;account.updated.Tests
tests/a-hold-stripe-declined-for-us-is-released.test.ts, 23 tests. Signed events go through the real route, with Stripe's API faked andwallet_reserve_card_authemulated statement by statement from 0155..createdreleasing the late hold;.created;.created:.createdand.updated;wallet_close_card_authpath..updatedon a pending authorization:network_fallbackdecline waits for the close, and the close releases it;network_fallbackkeeps the hold..creatednot routed;approvedinstead of the request's;network_fallbackcounted as a decline;.updated;Independent review, two rounds:
Round 1:
network_fallback;.updated;All addressed.
Round 2: no new way for capturable money to become spendable.
network_fallbackauthorization open for good, which would strand its holds. That also happens when the network declined too, the usual case without a dedicated BIN. Its hold now waits only for the close.Local validation on
8fd4d4c2e:tscis clean, lint has 0 errors (its 15 warnings are all in files this PR does not touch), the i18n gate is clean, and the full vitest suite passes on Node 24.21, the repo's pin: 1,898 files, 25,899 tests.Not covered (residuals)
The decision is still made by an API call inside the 2-second window. Stripe's current pattern is to respond to the webhook directly with
{ approved }, which drops a network round trip from the window. That changes the handler's contract, and it is left for a decision.The account's timeout setting. On
approve, a purchase we would refuse can still be approved, and then captured with no hold.declineis the safe setting for this app.Holds that can still strand:
.request: 0155 treats only aprocessinghold as already reserved, so a replay after the hold was released reserves again (0342's replay note).A periodic reconcile of
processingholds against Stripe would close all three.A capture after the close of a network-approved (
network_fallback) authorization is debited with no hold left, like any late capture (Money: card holds follow each authorization request and each capture, in any event order (0487, stacked on #925) #954's expired residual).The billing webhook claims every event type in the shared
stripe_webhook_eventsledger. If it is ever subscribed to Issuing events, it can consume one first, and this endpoint then answers duplicate (pre-existing).docs/AGENT_HANDOFF.mdaround line 3179 is a historical changelog that names the events the route handled at the time. It is left as written.🤖 Generated with Claude Code
https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
Generated by Claude Code