Skip to content

feat: sell Workshop Tickets through CourseBuilder checkout - #82

Merged
joelhooks merged 2 commits into
mainfrom
feat/coursebuilder-workshop-checkout
Oct 9, 2026
Merged

joelhooks merged 2 commits into
mainfrom
feat/coursebuilder-workshop-checkout

Conversation

@shitratgit

@shitratgit shitratgit Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

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.
  • Pricing: CourseBuilder prices-formatted handles 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's openEnrollment / closeEnrollment. Without closeEnrollment, sales close at the Cohort start.
  • Checkout: signed-out Viewers get Clerk's sign-in modal and return to the same URL. Signed-in Viewers POST to /api/coursebuilder/checkout/stripe and go to Stripe Checkout.
  • Fulfillment: the Stripe webhook records MerchantCharge / MerchantSession / Purchase in 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.
  • Operator: /api/products now honors the fields cb product create|update already sends (type, slug, state, visibility, quantityAvailable), plus enrollment dates and resourceId. New cb trpc procedures: commerce.createCohort, commerce.createCoupon, commerce.seedMerchantCoupons (refuses a live key unless allowLive) and commerce.listPurchases.
  • docs/sop/workshop-launch.md: env vars per Netlify context, the Stripe webhook, cb setup, 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

  1. Fulfillment runs in-process, not in Inngest. Core 1.2.1's webhook only sends stripe/checkout-session-completed to an Inngest client; an Inngest function writes the purchase. coursebuilder.config.ts had no inngest, so every completed checkout would have thrown and recorded nothing. CodeTV's Inngest app (apps/workflows) can't reach the CourseBuilder DB. So src/coursebuilder/fulfillment.ts runs core's own stripeCheckoutSessionComplete handler 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).
  2. CodeTV owns /api/coursebuilder/* (injectEndpoints: false plus src/pages/api/coursebuilder/[...coursebuilder].ts). It wraps core to fix these 1.2.1 problems:
    • The webhook skips signature verification when stripe-signature is missing, and doesn't await the 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-link compare x-skill-secret to SKILL_SECRET. With SKILL_SECRET unset, a request with no header passes. These now return 401 unless SKILL_SECRET is set.
    • Checkout mints a Stripe promotion code from any couponId it's given, and auto-applies PPP from the query country. The browser now sends only usedCouponId and a ppp=1 opt-in. The server picks the merchant coupon and pins the country to US unless PPP was chosen and Netlify geolocation qualifies.
    • Checkout trusts userId, quantity and country from the query. The server now sets them.
  3. Stripe success/cancel URLs use DEPLOY_PRIME_URL on deploy previews. URL is always the production domain on Netlify. Production behavior is unchanged.
  4. Tests use node --test (Node 24 strips types natively). The repo had no test setup, so no framework was added: just a test script in apps/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 to main (all pre-existing in actions/index.ts, hackathon, supporter and youtube files). None in changed files.

  • astro build: passes.

  • Local astro dev probes, with placeholder Stripe values and no DB:

    • Webhook: missing or forged signature returns 400. A validly signed event reaches core and then the inline runner (purchase.flow.started, commerce.fulfillment.step), and returns 400 when the DB is unreachable.
    • Core support actions: refund and transfer return 401.
    • Signed-out checkout redirects back with ?checkout=sign-in-required. A foreign cancelUrl falls back to /.
    • /thanks/purchase renders Purchase Processing and degrades to "processing" when the DB is down.
    • Operator tRPC without a token returns 401.
  • Core's real prices-formatted, fed the exact request bodies the route builds:

    Buyer Price
    US, no code $500
    India, PPP not chosen $500, PPP offered
    India, PPP chosen $125
    US, 50% code $250

Not verified

No Clerk or Stripe test keys and no DB were available, so these have not run against real services:

  • Workshop page rendering with real rows.
  • Creating a Stripe Checkout session.
  • A real webhook delivery and the purchase rows it writes.

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 draft stops sales.

SR 🐀

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.
@netlify

netlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for codetv-automations ready!

Name Link
🔨 Latest commit 27b289c
🔍 Latest deploy log https://app.netlify.com/projects/codetv-automations/deploys/6ac8618e5993480008d4dc15
😎 Deploy Preview https://deploy-preview-82--codetv-automations.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for codetv-links ready!

Name Link
🔨 Latest commit 27b289c
🔍 Latest deploy log https://app.netlify.com/projects/codetv-links/deploys/6ac8618ee2767f00076c5009
😎 Deploy Preview https://deploy-preview-82--codetv-links.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for codetv ready!

Name Link
🔨 Latest commit 27b289c
🔍 Latest deploy log https://app.netlify.com/projects/codetv/deploys/6ac8618e3c9da000084477ab
😎 Deploy Preview https://deploy-preview-82--codetv.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@joelhooks
joelhooks marked this pull request as ready for review October 9, 2026 03:43
@joelhooks
joelhooks merged commit d13da95 into main Oct 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant