Flows

Payment Flow

BizTech uses Stripe Checkout Sessions for all paid transactions — event registration fees and membership dues. The backend creates a session, the frontend redirects to Stripe's hosted checkout page, and a webhook confirms the payment.


Architecture

Frontend                      Backend (/payments)               Stripe
───────                      ──────────────────               ──────
POST /payments ───────────►  payment() handler
  { paymentType, email,
    eventID, year,            │ stripe.checkout.sessions.create()
    success_url, cancel_url } │────────────────────────────────► Session
                              │◄──────────────────────────────── { url }

Save checkoutLink on registration
◄─────────────────────────────┘ Return session.url

window.open(url, "_self") ──────────────────────────────────► Stripe Checkout

                              webhook() handler ◄─────────── checkout.session.completed
Verify signature
Read metadata.paymentType
Update DynamoDB state

Payment Types

The paymentType field in the request body determines the post-payment behavior:

paymentTypeWhen usedWebhook action
"Event"Paid event registrationUpdates registration status from incompleteregistered (or acceptedComplete for application events)
"Member"Membership purchase (existing user)Updates user in biztechUsers, creates member record in biztechMembers2026, creates profile
"UserMember"Membership + new Cognito signupCreates Cognito user, then user + member + profile records
"OAuthMember"Membership for OAuth userCreates user + member + profile records (no Cognito step)

Backend Endpoints

All handlers in services/payments/handler.js:

MethodPathAuthHandler
POST/paymentsCORS onlypayment() — creates Stripe Checkout session
POST/payments/webhookStripe signaturewebhook() — handles checkout.session.completed
POST/payments/cancelStripe signaturecancel() — handles session expiration (currently disabled)

Session Creation payment()

File: services/payments/handler.js (line ~362)

Pricing logic

  • Event payments: Fetches the event from biztechEvents and the user from biztechUsers. Uses pricing.members or pricing.nonMembers based on user.isMember. Multiplied by 100 for cents.
  • Membership payments: Fixed MEMBERSHIP_PRICE = 1500 cents ($15.00 CAD). UBC students receive a $3 discount (unit_amount = 1200). Defined in services/payments/constants.js.

Session parameters

stripe.checkout.sessions.create({
  payment_method_types: ["card"],
  line_items: [{ price_data: { currency: "CAD", unit_amount, product_data }, quantity: 1 }],
  mode: "payment",
  metadata: { paymentType, email, fname, eventID, year, ... },
  success_url,
  cancel_url,
  expires_at: Math.floor(Date.now() / 1000) + 1800, // 30 minutes
  allow_promotion_codes: true,
});

All user form data is stored in the session metadata so the webhook can process it without a second database lookup.

For event payments, the returned session.url is also saved as checkoutLink on the registration record via updateHelper(). This allows re-registration to reuse the existing session URL.


Webhook webhook()

File: services/payments/handler.js (line ~36)

  1. Verifies the Stripe signature using endpointSecret
  2. Handles only checkout.session.completed events
  3. Reads metadata.paymentType and dispatches to the matching helper:
Helper functionPayment typeActions
eventRegistration()"Event"Queries registration, updates status incompleteregistered (or acceptedComplete)
memberSignup()"Member"Updates biztechUsers, creates biztechMembers2026 record, creates profile via createProfile()
userMemberSignup()"UserMember"Cognito signup → user + member + profile record creation
OAuthMemberSignup()"OAuthMember"User + member + profile record creation (no Cognito)

Frontend Integration

Registration Strategy Pattern

The frontend uses an abstract RegistrationStrategy class to manage registration and payment state.

File: src/lib/registrationStrategy/registrationStrategy.ts

abstract class RegistrationStrategy {
  abstract needsPayment(): boolean
  abstract regForPaid(): Promise<{ paymentUrl?: string }>
  abstract regForPaidApp(): Promise<{ paymentUrl?: string }>
  abstract confirmAndPay(): Promise<{ paymentUrl?: string }>
  // ...
}

Concrete implementation: RegistrationStateOld in src/lib/registrationStrategy/registrationStateOld.ts

MethodWhen usedWhat it does
regForPaid()Non-application paid event, first registrationCreates registration with status INCOMPLETE, then POSTs to /payments
regForPaidApp()Application-based paid eventSame + sets applicationStatus: REVIEWING
confirmAndPay()User accepted, needs to payPOSTs to /payments for a new checkout session
needsPayment()State checkReturns true if status is INCOMPLETE or ACCEPTED

Checkout redirect

All three payment methods POST to /payments and redirect with:

const response = await fetchBackend('/payments', 'POST', paymentBody)
window.open(response, '_self') // redirects to Stripe hosted page

The success_url is constructed as CLIENT_URL/event/{eventId}/{year}/register/success.

Re-registration shortcut

In services/registrations/handler.js (line ~366): if a user tries to register but already has an incomplete registration, the backend returns { url: existingReg.checkoutLink } — the previously-saved Stripe session URL. The frontend strategy checks for res.url and returns it directly as paymentUrl before calling /payments.


Environment Variables

Defined in services/payments/serverless.yml:

VariablePurpose
STRIPE_DEV_KEY / STRIPE_PROD_KEYStripe secret API key
STRIPE_DEV_ENDPOINT / STRIPE_PROD_ENDPOINTWebhook signing secret for checkout.session.completed
STRIPE_DEV_CANCEL / STRIPE_PROD_CANCELWebhook signing secret for cancel/expiration
ENVIRONMENT"PROD" selects production keys; anything else uses dev keys

Stripe is initialized at the top of handler.js:

const stripe = require('stripe')(
  process.env.ENVIRONMENT === 'PROD'
    ? process.env.STRIPE_PROD_KEY
    : process.env.STRIPE_DEV_KEY,
)

DynamoDB Tables

TableUsage in payment flow
biztechEventsLookup pricing.members / pricing.nonMembers for event payments
biztechUsersLookup isMember for pricing tier; updated on membership payment
biztechMembers2026Created on membership payment completion
biztechRegistrationsStores registrationStatus and checkoutLink (Stripe session URL)
biztechProfilesProfile created as side effect of membership payment

Key Files

FilePurpose
services/payments/handler.jsAll 3 endpoint handlers + 4 webhook helper functions
services/payments/constants.jsMEMBERSHIP_PRICE = 1500
services/payments/serverless.ymlEndpoint definitions, env vars, IAM permissions
src/lib/registrationStrategy/registrationStrategy.tsAbstract strategy base class
src/lib/registrationStrategy/registrationStateOld.tsConcrete payment + registration logic
src/pages/membership.tsxMembership payment flow
src/pages/event/[eventId]/[year]/register/index.tsxEvent registration payment flow

Previous
Registration