feat(payments): Phase 0b β create-stripe-subscription + fix webhook template_user_id (closes #102)#111
Merged
Conversation
β¦emplate_user_id (#102) Second sub-issue of the Phase 0 epic (#100). Ships create-stripe- subscription AND fixes a paired bug in the existing stripe-webhook that would have prevented the subscription row from being inserted. WHAT SHIPS 1. `supabase/functions/create-stripe-subscription/` (new) β POST endpoint: Body: { price_id: string, customer_email: string } Auth: required Logic: - JWT β user_id - Validate price_id is non-empty, email matches the same regex used in payment-service.ts - Call stripe.checkout.sessions.create with: - mode: 'subscription' - line_items: [{ price: price_id, quantity: 1 }] - customer_email - subscription_data.metadata: { template_user_id: <caller user_id>, customer_email: <validated email> } - success_url: /payment-result?session_id={...}&status=subscribed - cancel_url: /payment-result?status=cancelled - Return { sessionId } 2. `supabase/functions/stripe-webhook/index.ts` (modified) β handleSubscriptionEvent now reads template_user_id from subscription.metadata and writes it on the upsert. Without this fix the NOT NULL constraint on subscriptions.template_user_id fails on `customer.subscription.created`. Also returns {handled:false} with a logged error when the metadata is absent (defensive β means the subscription was created outside our flow). 3. `docs/PAYMENT-DEPLOYMENT.md` β status updated to "Phase 0a + 0b shipped"; Step 4.2 lists the three deployable functions; operator note added about re-deploying stripe-webhook after this PR lands (the handler changed alongside the new function). WHY THE WEBHOOK CHANGE IS REQUIRED, NOT OPTIONAL The `subscriptions` table requires (verified in monolithic migration): - template_user_id UUID NOT NULL REFERENCES auth.users(id) - provider_subscription_id TEXT NOT NULL UNIQUE - customer_email TEXT NOT NULL - plan_amount INTEGER NOT NULL CHECK (plan_amount >= 100) - plan_interval TEXT NOT NULL CHECK (plan_interval IN ('month', 'year')) - status TEXT NOT NULL CHECK (status IN ('active', 'past_due', 'grace_period', 'canceled', 'expired')) The existing webhook handleSubscriptionEvent NEVER set template_user_id, so any customer.subscription.created event would have failed at INSERT with a constraint violation. The bug was latent because no production flow existed to trigger it (no caller of create-stripe-subscription was working before this PR). Phase 0b lights up that path, so the webhook fix is required to keep it working. WHY NO subscriptions ROW IS INSERTED HERE At session-creation time we don't have a provider_subscription_id β Stripe assigns it post-checkout when the customer actually pays. The table's provider_subscription_id is NOT NULL UNIQUE, so we can't pre-insert with a placeholder. The clean handoff is: create-stripe-subscription: β Creates Stripe Checkout Session β Returns sessionId β (Browser redirects user to Stripe) Customer completes checkout β Stripe fires: β customer.subscription.created with subscription.metadata.{template_user_id, customer_email} β stripe-webhook (this PR's fix) upserts subscriptions row with status='active' WHAT THIS PR DOES NOT DO - β Phase 0c (PayPal one-off) β #103 - β Phase 0d (PayPal subscription) β #104 - β Phase 0e (cancel + resume subscription) β #105 - β Un-skip E2E payment tests β #106 - β Touch any schema (existing subscriptions table is already correct) - β Change the browser-side createSubscriptionCheckout (already passes price_id + customer_email per the contract this function implements; the Authorization header was added in Phase 0a's #110) VERIFICATION - Type-check clean (strict mode) - Lint clean - 81/81 payment unit tests pass (no regression) - 302/302 test files, 3329 unit tests pass - Deno contract tests document the request/response shape + the webhook-metadata pairing requirement OPERATOR NEXT STEPS POST-MERGE supabase functions deploy create-stripe-subscription supabase functions deploy stripe-webhook # re-deploy due to handler fix Then the SubscriptionManager component subscribe flow works end-to-end against Stripe sandbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Second sub-issue of the Phase 0 epic (#100). Ships
create-stripe-subscriptionAND fixes a paired bug in the existingstripe-webhookthat would prevent subscription rows from being inserted.Closes #102.
Why a webhook fix lives in the same PR
The
subscriptionstable requirestemplate_user_id NOT NULL. The existinghandleSubscriptionEventinstripe-webhooknever set it β the bug was latent because no production flow lit up the subscription path. Phase 0b activates that path, so the webhook fix is required to keep it working.What ships
supabase/functions/create-stripe-subscription/POST {price_id, customer_email}β creates Stripe Checkout Session in subscription mode β returns{sessionId}. Setssubscription_data.metadata.template_user_idso the webhook can satisfy the NOT NULL constraint.supabase/functions/stripe-webhook/index.tshandleSubscriptionEventnow readstemplate_user_id+customer_emailfromsubscription.metadataand writes both on the upsert. Returns{handled:false}(with logged error) when metadata is absent.docs/PAYMENT-DEPLOYMENT.mdstripe-webhookmust be re-deployed after this PR lands.Why no
subscriptionsrow is inserted at session-creation timeAt session-creation we don't have
provider_subscription_idβ Stripe assigns it post-checkout when the customer completes payment. The table'sprovider_subscription_idisNOT NULL UNIQUE. The clean handoff:Security model
Identical to Phase 0a:
NEXT_PUBLIC_SUPABASE_ANON_KEYuser_idflows into Stripe metadata so the webhook can attribute the subscription row to the right userpayment-service.tsregexOperator deploy steps post-merge
supabase functions deploy create-stripe-subscription supabase functions deploy stripe-webhook # re-deploy due to handler fixThen
SubscriptionManagersubscribe flow works end-to-end against Stripe sandbox.What this PR does NOT do
createSubscriptionCheckoutcaller alreadypasses
price_id+customer_emailper the contract this functionimplements; Authorization header was added in Phase 0a's feat(payments): Phase 0a β create-stripe-checkout + verify-stripe-session (closes #101)Β #110)
Test plan
π€ Generated with Claude Code