Repository navigation
feat: sell Workshop Tickets through CourseBuilder checkout - #82
Merged
Merged
Conversation
Adds the minimum phase-5 commerce path for a first Cohort:
- /workshops/{slug}: Workshop page with Cohort dates and a pricing
island fed by CourseBuilder prices-formatted (default/early bird
coupon, ?code= coupon, opt-in PPP) and an enrollment window
(open / not yet open / closed at Cohort day one).
- Sign-in-gated checkout through a CodeTV-owned /api/coursebuilder/*
route (injectEndpoints: false). The server sets the buyer, quantity,
country and merchant coupon; the browser sends only the site coupon
and a PPP opt-in.
- Webhook fulfillment runs core's stripeCheckoutSessionComplete handler
in-process (CodeTV's Inngest app has no access to the CourseBuilder
DB). Failures return non-2xx so Stripe retries.
- /thanks/purchase: Purchase Processing screen that polls
/api/commerce/checkout-status, then shows the welcome state, with the
"payment succeeded, setup delayed" fallback after ~2 minutes.
- Operator: /api/products honors the fields cb already sends plus
enrollment dates and resource links; cb trpc procedures
commerce.createCohort, commerce.createCoupon,
commerce.seedMerchantCoupons and commerce.listPurchases.
- Stripe success/cancel URLs use DEPLOY_PRIME_URL on Netlify previews.
- docs/sop/workshop-launch.md: env, webhook, cb setup, test purchase.
Guards for @coursebuilder/core 1.2.1 problems:
- webhook signatures are verified before core (core skips the check
without the header and does not await it with one);
- refund/transfer/lookup/create-magic-link return 401 unless
SKILL_SECRET is set (core treats unset as a match);
- checkout never trusts browser couponId, userId, quantity or country.
Tests: node --test (44 cases) for enrollment, polling, coupon decisions,
redirect guards and inline fulfillment against core's real handler.
✅ Deploy Preview for codetv-automations ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for codetv-links ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for codetv ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
joelhooks
marked this pull request as ready for review
October 9, 2026 03:43
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.
Phase 5 of
plans/coursebuilder-codetv-api-surface.md, cut to the minimum needed to sell a first Cohort. Draft: customer-facing money code, and it has not run a real Stripe test purchase yet (see "Not verified").What it does
/workshops/{slug}: Workshop page with Cohort dates, a placeholder "what's included" list, and a pricing island. Copy is placeholder.prices-formattedhandles the default (early bird) coupon, a?code=coupon, and PPP. PPP is opt-in, as in AI Hero. The enrollment window (open/not-yet-open/closed) comes from the product'sopenEnrollment/closeEnrollment. WithoutcloseEnrollment, sales close at the Cohort start./api/coursebuilder/checkout/stripeand go to Stripe Checkout.MerchantCharge/MerchantSession/Purchasein the CodeTV CourseBuilder DB. Workshop Access is purchase-based./thanks/purchase: the Purchase Processing screen. It polls/api/commerce/checkout-status, shows the welcome state once the purchase is verified, and falls back to the "payment succeeded, setup delayed" message after ~2 minutes./api/productsnow honors the fieldscb product create|updatealready sends (type,slug,state,visibility,quantityAvailable), plus enrollment dates andresourceId. Newcb trpcprocedures:commerce.createCohort,commerce.createCoupon,commerce.seedMerchantCoupons(refuses a live key unlessallowLive) andcommerce.listPurchases.docs/sop/workshop-launch.md: env vars per Netlify context, the Stripe webhook,cbsetup, a test-mode purchase on a deploy preview, and rollback.Jason's membership checkout (
src/pages/api/stripe/*), the Clerk setup and Sanity are untouched.Design calls worth reviewing
stripe/checkout-session-completedto an Inngest client; an Inngest function writes the purchase.coursebuilder.config.tshad noinngest, so every completed checkout would have thrown and recorded nothing. CodeTV's Inngest app (apps/workflows) can't reach the CourseBuilder DB. Sosrc/coursebuilder/fulfillment.tsruns core's ownstripeCheckoutSessionCompletehandler inside the webhook request. Failures return non-2xx, so Stripe retries, and core dedupes by charge. Moving to Inngest later means swapping that one object (TODO in the file)./api/coursebuilder/*(injectEndpoints: falseplussrc/pages/api/coursebuilder/[...coursebuilder].ts). It wraps core to fix these 1.2.1 problems:stripe-signatureis missing, and doesn'tawaitthe check when it's present. A forged event was processed in a local probe. The route now verifies the raw body first.refund/transfer/lookup/create-magic-linkcomparex-skill-secrettoSKILL_SECRET. WithSKILL_SECRETunset, a request with no header passes. These now return 401 unlessSKILL_SECRETis set.couponIdit's given, and auto-applies PPP from the query country. The browser now sends onlyusedCouponIdand appp=1opt-in. The server picks the merchant coupon and pins the country toUSunless PPP was chosen and Netlify geolocation qualifies.userId,quantityandcountryfrom the query. The server now sets them.DEPLOY_PRIME_URLon deploy previews.URLis always the production domain on Netlify. Production behavior is unchanged.node --test(Node 24 strips types natively). The repo had no test setup, so no framework was added: just atestscript inapps/website/package.json.Checks
pnpm --filter @codetv/website test: 44 passing. Covers the enrollment window, polling schedule, coupon/PPP decisions, redirect guards, and inline fulfillment against core's real handler with a fake adapter and Stripe session.astro check: 11 errors, identical tomain(all pre-existing inactions/index.ts, hackathon, supporter and youtube files). None in changed files.astro build: passes.Local
astro devprobes, with placeholder Stripe values and no DB:purchase.flow.started,commerce.fulfillment.step), and returns 400 when the DB is unreachable.refundandtransferreturn 401.?checkout=sign-in-required. A foreigncancelUrlfalls back to/./thanks/purchaserenders Purchase Processing and degrades to "processing" when the DB is down.Core's real
prices-formatted, fed the exact request bodies the route builds:Not verified
No Clerk or Stripe test keys and no DB were available, so these have not run against real services:
The SOP's "Test purchase on a deploy preview" section is the proof to run before merging. It needs
COURSEBUILDER_STRIPE_*test keys set for deploy previews in Netlify, a test-mode webhook endpoint, and Clerk working on the preview domain.Out of scope (TODOs name the AI Hero files to port)
Team seats and
/dashboard/team, Ticket Transfer, the CodeTV Invoice page, purchase emails, Discord role grant, Kit sync.Deploy and rollback
Nothing goes live until this merges and Netlify has the
COURSEBUILDER_STRIPE_*env for production. Rollback before merge: close the PR. After merge:cb product update <productId> --state draftstops sales.SR 🐀