Events

Event Pricing and Payments

Events can be free or paid. Paid events use Stripe Checkout with a price determined by the event record's pricing field and the user's membership status.


Pricing Model

Each event stores pricing in a pricing object:

{
  "pricing": {
    "members": 0,
    "nonMembers": 5
  }
}
FieldTypeUnitDescription
pricing.membersNumberDollarsPrice for BizTech members
pricing.nonMembersNumberDollarsPrice for non-members

A value of 0 means the event is free for that group. A value of 5 means $5.00 CAD.


Payment Flow

User clicks "Register"


Frontend checks if event is free

        ├── pricing === 0Register directly → status: "registered"

        └── pricing > 0Create Stripe session


           POST /payments/
           { paymentType: "Event", eventID, year, email }


           Backend fetches event → reads pricing


           Creates Stripe Checkout session
           unit_amount = pricing × 100 (cents)


           Updates registration → status: "incomplete"
           Stores checkoutLink on registration record


           Redirects user to Stripe Checkout

                ├── User pays → Stripe webhook → status: "registered"
                └── User abandons → session expires (30 min) → stays "incomplete"

How the Backend Reads Event Pricing

File: services/payments/handler.jsexport const payment

When the payment type is "Event":

const [event, user] = await Promise.all([
  db.getOne(data.eventID, EVENTS_TABLE, { year: Number(data.year) }),
  db.getOne(data.email, USERS_TABLE),
])

const isMember = !isEmpty(user) && user.isMember
const samePricing = event.pricing.members === event.pricing.nonMembers

unit_amount =
  (isMember ? event.pricing.members : event.pricing.nonMembers) * 100
StepWhat happens
Fetch event and user in parallelGets the event record and user's membership status
Determine isMemberChecks user.isMember flag
Select pricepricing.members for members, pricing.nonMembers for non-members
Convert to centsMultiplies by 100 for Stripe's unit_amount

The event's display name and image are also used:

data.paymentName = `${event.ename} ${
  isMember || samePricing ? '' : '(Non-member)'
}`
data.paymentImages = [event.imageUrl]

Stripe Checkout Session

const session = await stripe.checkout.sessions.create({
  payment_method_types: ['card'],
  line_items: [
    {
      price_data: {
        currency: 'CAD',
        product_data: {
          name: data.paymentName,
          images: paymentImages,
        },
        unit_amount,
      },
      quantity: 1,
    },
  ],
  metadata: data,
  mode: 'payment',
  success_url: data.success_url,
  cancel_url: data.cancel_url,
  expires_at: Math.round(new Date().getTime() / 1000) + 1800,
  allow_promotion_codes: true,
})
SettingValue
CurrencyCAD
Session expiry30 minutes
Promotion codesEnabled
Product nameEvent ename + "(Non-member)" suffix if applicable
Product imageEvent imageUrl

After session creation, the handler updates the registration with the checkout link:

await updateHelper(
  { eventID, year, checkoutLink: session.url },
  false,
  email,
  fname,
)

This sets the registration status to "incomplete" until payment completes.


Application-Based Events with Pricing

For application-based events (isApplicationBased: true), the flow is different:

Register"registered" (pending review)

   Admin accepts

   ├── pricing === 0"acceptedPending" (no payment needed)
   └── pricing > 0"accepted" → user pays → "acceptedComplete"

The acceptance handler checks:

const pricing = isMember
  ? eventExists.pricing?.members ?? 0
  : eventExists.pricing?.nonMembers ?? 0

if (pricing === 0) {
  data.registrationStatus = 'acceptedPending'
}

Free Events

When pricing.members and pricing.nonMembers are both 0, the event is free:

  • No Stripe session is created
  • Registration goes directly to "registered" status
  • No checkout link is stored
  • No webhook processing needed

Where Pricing Is Set

Pricing is configured by admins during event creation or editing:

Frontend: src/components/Events/EventForm.tsx

  • price field → maps to pricing.members
  • nonMemberPrice field → maps to pricing.nonMembers
  • Defaults to 0 (free) if not set

Backend: services/events/handler.jscreate / update

  • Stored as-is in the event record
  • No backend validation on pricing values beyond type checking

Key Files

FilePurpose
services/payments/handler.jspaymentCreates Stripe checkout session using event pricing
services/payments/handler.js → webhookProcesses Stripe webhook, updates registration status
services/registrations/handler.jsupdateHelperStores checkout link, handles status transitions
services/events/handler.jscreate, updateStores pricing on event record
src/components/Events/EventForm.tsxAdmin pricing input fields

Previous
Registrations