Repository navigation
fix(#1184): hosted checkout can be fulfilled — correlate by client_reference_id - #1191
Merged
Merged
Conversation
…ference_id
`handlePaymentCheckout` looked the intent up by `session.metadata?.intent_id`, and
`create-stripe-checkout` never sets session-level metadata. It sets
`payment_intent_data.metadata.intent_id` — which lands on the PaymentIntent, not the
Session — and `client_reference_id`. So the lookup was always undefined, the handler always
returned {handled:false}, and the path real buyers take could not be fulfilled.
MEASURED, NOT INFERRED. A real live Checkout Session created by the deployed function on
2026-09-17 came back with `metadata: {}` and
`client_reference_id: 516bf680-e568-445a-98da-6a04a40d9589` — our intent id, present and
correct. Meanwhile `payment_results` held 0 rows for the life of the project and every
historical `payment_intent.succeeded` recorded `related_payment_id: null`, i.e. it too
returned {handled:false} every time. Neither fulfilment path had ever executed successfully.
`verify-stripe-session` is read-only and writes nothing, so there was no third path.
Reading `client_reference_id` as the fallback also repairs sessions created BEFORE this fix,
because it was always being set. Session metadata still wins when genuinely populated, for
forward compatibility.
BOTH HANDLERS CAN NOW WRITE THE SAME ROW, which was impossible while one of them was dead.
idx_payment_results_one_succeeded_per_intent permits ONE succeeded row per intent, so
whichever Stripe event arrives second collides. A 500 there is retried for three days and
counts toward the endpoint being disabled — the exact pressure that nearly cost us the
webhook last night. Both inserts now acknowledge 23505 and still call advanceOrderAndNotify,
which is a compare-and-swap on status and returns 'already-advanced' rather than emailing
the buyer twice.
NOT ADDING SESSION METADATA to create-stripe-checkout. client_reference_id already carries
it, is present on historical sessions, and needs no redeploy of the outbound function.
Verified: 6/6 new, 15/15 with advance-order, 985/985 test:scripts, `deno check` clean via
denoland/deno in Docker. The mutation bites — removing the client_reference_id fallback
fails 3 of 6 tests including the control.
Refs #1184
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8 tasks
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.
Closes #1192
Refs #1184
Hosted checkout — the path real buyers take — could not be fulfilled.
handlePaymentCheckoutlooked the intent up by
session.metadata?.intent_id, andcreate-stripe-checkoutnever setssession-level metadata. It sets
payment_intent_data.metadata.intent_id(which lands on thePaymentIntent, not the Session) and
client_reference_id.Measured, not inferred
A real live Checkout Session created by the deployed function on 2026-09-17:
And the corroborating state:
payment_resultspayment_intent.succeededrelated_payment_id: null—{handled:false}every timeverify-stripe-sessionpayment_resultsis "the source of truth"So neither fulfilment path had ever executed successfully, and there was no third path. A
customer who paid would have got: no payment row, an order stuck at
pending, and no receipt.The fix
Correlate by
client_reference_idwhen session metadata is absent. That also repairs sessionscreated before this change, because the field was always being set. Explicit session metadata
still wins, for forward compatibility.
The companion change is not optional. With both handlers alive they both write
payment_results, andidx_payment_results_one_succeeded_per_intentpermits onesucceededrowper intent — so whichever Stripe event arrives second collides. A 500 there is retried for three
days and counts toward the endpoint being disabled, which is the exact pressure that nearly cost us
the webhook last night. Both inserts now acknowledge
23505and still calladvanceOrderAndNotify, which is a compare-and-swap onstatusand returnsalready-advancedrather than emailing the buyer twice.
Deliberately not adding session metadata to
create-stripe-checkout:client_reference_idalready carries it, works on historical sessions, and needs no redeploy of the outbound function.
Verification
advance-order, 985/985test:scripts,deno checkclean.client_reference_idfallback fails 3 of 6 tests, includingthe control that proves the resolver can return both a value and null.
{}is truthy, so a naivesession.metadata ? …check would still resolve to
undefined. That is the bug, and a test now fails on it.This is a prerequisite for the go-live flip in #629/#1185: flipping the publishable key while this
was broken would have meant taking real money with nothing recorded.
🤖 Generated with Claude Code