Skip to content

[FEATURE] Payment Handling #38

Open

Description

@StevScripts

Feature Description

Let people pay GPK dues on the site through Stripe, and grab their name, email, and shirt size while they're at it so we can order shirts without chasing anyone down.

I want to keep this small on purpose. Phase 1 is a Membership section on the homepage with a "Pay dues" button that bounces you to a Stripe hosted checkout page. No login and no database. Whoever is handling the money opens the Stripe Dashboard and sees who paid and what size they picked.

Problem / Motivation

Right now the only "join" button on the site is the Discord invite in the hero. There's no way to actually pay dues.

So every term dues and shirt sizes get collected in separate places and somebody has to match them up by hand. Shirt orders need one list of names and sizes we trust, and the easiest way to get that is to make the payment itself carry the size.

Also, #32 installed drizzle-orm, postgres, and drizzle-kit "so we can start building the database layer" and nothing has touched them since. This gives that work somewhere to go later without making it a blocker now.

Proposed Solution

What a student sees

  1. A new #membership section on the homepage: the price, what you get, a "Pay dues" button. Maybe a second button next to "Join Us" in the hero, up to whoever builds it.
  2. The button posts to /api/checkout. That creates a Stripe Checkout Session and redirects to Stripe's page.
  3. Stripe's page asks for email and name on its own. We add two custom fields:
    • T-shirt size, required dropdown (XS, S, M, L, XL, 2XL, 3XL)
    • Discord username, optional text, so we can hand out roles later
  4. After paying they land on a short "you're in, here's the Discord" page.

What officers see

Every payment in Dashboard > Payments shows the shirt size and Discord name. We create the session with customer_creation: "always" so each payer becomes a Customer in Stripe, which means the Customers tab is basically the member list. The payments export can include the custom field columns, and that export is the shirt order.

One Stripe Product per term ("GPK Membership - Fall 2026") so filtering by semester is a dropdown in the Dashboard instead of a spreadsheet problem.

Implementation sketch

Should fit in one PR under the ~300 line guideline from CONTRIBUTING.

  • npm i stripe. Server SDK only, hosted Checkout doesn't need anything client side.
  • lib/stripe.ts with a lazily created client. Don't do new Stripe(process.env.STRIPE_SECRET_KEY!) at the top of the module. CI and the Docker build run with zero env vars and will fall over.
  • app/api/checkout/route.ts (POST). Creates the session with mode: "payment", line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }], customer_creation: "always", the two custom fields above, metadata: { term }, success_url, cancel_url. Redirect to session.url.
  • components/MembershipSection.tsx plus a <section id="membership"> in app/page.tsx. The sections array in Navbar.tsx and the Club links in Footer.tsx are both hardcoded, so add "membership" to both.
  • app/join/success/page.tsx, or just a query param banner on / if that's less hassle.
  • .env.example with STRIPE_SECRET_KEY, STRIPE_PRICE_ID, NEXT_PUBLIC_SITE_URL. Heads up: .gitignore has .env* which would swallow the example file, so add !.env.example. Add env_file: .env to the gpkweb service in docker-compose.yml.
  • Styling: the tokens and the pill button / card classes from HeroSection.tsx and ChallengeCard.tsx already cover this. Please don't invent new ones.

Done when

  • In Stripe test mode you can click "Pay dues" on the site, pay with a test card, and land on the success page.
  • The payment in the Dashboard shows name, email, shirt size, Discord username, and term.
  • Each payer has a Customer record.
  • Shirt size is required, Discord username isn't.
  • npm run lint and npm run build pass with no Stripe env vars set, because CI has none.
  • Navbar highlights Membership when you scroll to it and the footer links to it.
  • No secrets in the repo. .env.example lists every variable.

Things we need to decide first

Decision Who Default if nobody says otherwise
Price, and per semester vs per year Treasurer + President One payment per term, no subscriptions
Whose Stripe account this is, and whether the club is allowed to collect dues into it under UCF RSO/SGA rules Treasurer Figure out before merge; the code doesn't care either way
Shirt sizes Treasurer XS through 3XL
Pass the card fee on to the member? Treasurer No, bake it into the price
Homepage section or a separate /join page Dev lead Homepage section. The site is one page and the Navbar hash links only work on /

Not in this issue

  • The webhook. A POST /api/stripe/webhook listening for checkout.session.completed and writing to a Drizzle members table (finally using the stuff from chore: install drizzle orm dependencies #32), plus some protected roster or CSV page. Needs a real host with a public HTTPS URL and a signing secret, and we don't have either yet. Also has to be idempotent since Stripe retries. Separate issue when we get there.
  • Discord bot role sync, login, a "my membership" page. Later, once phase 1 exists and we know where this is hosted.
  • Subscriptions. Students come and go every semester and refunding a one time charge is way less annoying than cancelling a subscription.

Alternatives Considered

The lazy option is a Stripe Payment Link. Make the product and the shirt size dropdown in the Dashboard and just link to it, zero code. Honestly this gets most of the way there. Downsides: dropdowns made in the Dashboard cap at 10 options (fine for us), you can't set customer_creation from the Dashboard so people show up as guest customers, and there's no path to the webhook later. If we want dues live before the PR lands, do this first and swap the button to /api/checkout afterward. Nobody will notice.

The other extreme is a full membership portal with login, a DB, and an admin dashboard. I don't want to go there yet. The repo has no auth, no env setup, no schema, no known production host, and no webhook URL, and phase 1 needs none of that. I'd rather ship something.

Google Form plus Venmo or Zelle is what we're trying to get away from. No proof of payment and someone still has to reconcile by hand.

Embedded Checkout would mean more UI work and pulling in @stripe/stripe-js just to render a form with one required field, so hosted it is.

Additional Context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions