Skip to content

[BACKEND] Stripe Checkout Session creation endpoint for paid social sessions #64

Description

@aleckshen

Is your feature request related to a problem? Please describe

Members and guests can register for social sessions, and SocialSessions already carries memberPrice / nonMemberPrice, but there is no backend path that turns a registration intent into a payable Stripe session. Today a user hitting a paid session has nowhere to be charged: SocialSessionRegistrations exposes paymentStatus (pending | paid | free) and amountPaid, but nothing ever sets them to paid. We need an endpoint that creates a Stripe Checkout Session, stores enough state on our side to reconcile it later, and returns a redirect URL the frontend can send the user to.

Describe the solution you'd like

Add a backend endpoint (e.g. POST /api/social-sessions/:id/checkout, implemented as a Payload custom endpoint on SocialSessions alongside the existing /:id/availability endpoint at src/payload/endpoints/socialSessionAvailability.ts:1) that:

  1. Resolves price. Look up the parent SocialSessions doc. Pick memberPrice if req.user?.collection === "users", otherwise nonMemberPrice. If the resolved price is null/0, reject — free sessions should go through the existing direct-create path on SocialSessionRegistrations, not Stripe.

  2. Pre-flights capacity. Reuse the same checks as src/payload/hooks/socialSessionRegistrations/checkCapacity.ts:5 (status not completed/cancelled, not already registered, capacity / waitlist space available). Refactor the shared logic into a helper if needed so the hook and this endpoint stay in sync — we do not want to take payment for a session the hook will then reject.

  3. Creates a pending registration. Insert a social-session-registrations row with paymentStatus: "pending", amountPaid: <resolved price>, and registrationStatus set per capacity (registered or waitlisted). For guest flows accept guestName + guestEmail in the request body; for member flows take the user from req.user.

  4. Creates a Stripe Checkout Session using the existing client at src/lib/stripe.ts:7:

    • mode: "payment"
    • one line_item priced in NZD (unit_amount in cents) with the session title as the product name
    • customer_email set from the user / guest email so Stripe pre-fills it
    • metadata.registrationId = the Payload registration ID (this is the contract Ticket 2 reads)
    • metadata.socialSessionId for debuggability
    • success_url / cancel_url taken from env (NEXT_PUBLIC_SITE_URL + known paths) — leave the frontend routes as TODO placeholders if they do not yet exist; Ticket 2 does not depend on them
    • expires_at set so abandoned checkouts auto-expire (Stripe default 24h is fine; revisit if it crowds the waitlist)
  5. Returns { url, registrationId } so the frontend can redirect.

  6. Failure handling. If the Stripe call throws after the registration is created, delete the pending registration before returning the error so we don't leak phantom rows that occupy capacity.

Also: add a stripeCheckoutSessionId field to src/payload/collections/SocialSessionRegistrations.ts:63 (admin-only write, hidden from non-admin reads) so Ticket 2 can match webhook events back to a registration without relying solely on metadata. Run pnpm typegen after.

Required env: STRIPE_SECRET_KEY (already referenced), NEXT_PUBLIC_SITE_URL. Document both in the project README env section.

Describe alternatives you've considered

  • Stripe Payment Links — simplest, but no per-registration metadata and no way to reserve capacity before the user pays, so two users could pay for the last spot.
  • Payment Intents + custom UI — gives a nicer in-page experience, but Checkout Sessions are PCI-light, hosted by Stripe, and ship faster. Worth revisiting if/when we want a native checkout form.
  • Charging on the existing beforeChange hook — couples capacity logic to network I/O against Stripe and makes admin-side creates from the dashboard awkward. Keeping the endpoint separate keeps the hook fast and dashboard flows unchanged.

Additional context

  • Existing Stripe client: src/lib/stripe.ts:1
  • Capacity / dedupe logic to share: src/payload/hooks/socialSessionRegistrations/checkCapacity.ts:5
  • Confirmation email is fired from the capacity hook today (src/lib/email/registrationConfirmation.ts). For paid sessions we want the confirmation email to fire on payment success, not on pending creation — another ticket will handle this trigger.
  • Pricing fields are stored as plain numbers; treat them as NZD whole units and multiply by 100 for unit_amount.

Before merging

  • Code generation run (pnpm typegen)
  • PR reviewed (for non-trivial changes)
  • All required PR checks passing

Metadata

Metadata

Assignees

Labels

No labels
No labels
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions