Real Stripe TEST traffic (Phase 9.1) exposed a genuine defect the deterministic TestProvider
never could: payment_intent.payment_failed and charge.refunded events reference Stripe's
PaymentIntent/Charge object, never the Checkout Session, but payment_transactions.
provider_reference only ever stored the Session id. A real failure or refund webhook would have
silently failed to match any transaction (UNKNOWN_TRANSACTION).
- payment-provider.js: session creation now also sets payment_intent_data.metadata (the same
metadata already carrying our transaction_id), so it propagates onto the PaymentIntent Stripe
creates. normalizeEvent() (now async) reads transaction_id back from the PaymentIntent's own
metadata directly for payment_intent.payment_failed, and via one extra paymentIntents.retrieve
call for charge.refunded (Charges don't inherit PaymentIntent metadata automatically).
- commerce.js: the transaction lookup now matches provider_reference OR transaction_id — one
additional OR clause, no schema/migration change.
- webhook-routes.js: added the now-required await on verifyAndParseWebhook (TestProvider's
synchronous return is unaffected).
- docker-compose.dev.yml: wires PAYMENT_PROVIDER/STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET through
to the api container (previously entirely absent — Phase 9 never needed it against the
deterministic provider), empty/unset by default matching EMAIL_PROVIDER's own convention.
Verified fixed against a real Stripe TEST refund (Phase 9.1); e2e-phase9.js 20/20 and the Winter
Break commerce rehearsal 13/13 rerun clean after the fix.