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:
-
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.
-
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.
-
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.
-
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)
-
Returns { url, registrationId } so the frontend can redirect.
-
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
Is your feature request related to a problem? Please describe
Members and guests can register for social sessions, and
SocialSessionsalready carriesmemberPrice/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:SocialSessionRegistrationsexposespaymentStatus(pending|paid|free) andamountPaid, but nothing ever sets them topaid. 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 onSocialSessionsalongside the existing/:id/availabilityendpoint atsrc/payload/endpoints/socialSessionAvailability.ts:1) that:Resolves price. Look up the parent
SocialSessionsdoc. PickmemberPriceifreq.user?.collection === "users", otherwisenonMemberPrice. If the resolved price isnull/0, reject — free sessions should go through the existing direct-create path onSocialSessionRegistrations, not Stripe.Pre-flights capacity. Reuse the same checks as
src/payload/hooks/socialSessionRegistrations/checkCapacity.ts:5(status notcompleted/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.Creates a pending registration. Insert a
social-session-registrationsrow withpaymentStatus: "pending",amountPaid: <resolved price>, andregistrationStatusset per capacity (registeredorwaitlisted). For guest flows acceptguestName+guestEmailin the request body; for member flows take the user fromreq.user.Creates a Stripe Checkout Session using the existing client at
src/lib/stripe.ts:7:mode: "payment"line_itempriced in NZD (unit_amountin cents) with the session title as the product namecustomer_emailset from the user / guest email so Stripe pre-fills itmetadata.registrationId= the Payload registration ID (this is the contract Ticket 2 reads)metadata.socialSessionIdfor debuggabilitysuccess_url/cancel_urltaken 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 themexpires_atset so abandoned checkouts auto-expire (Stripe default 24h is fine; revisit if it crowds the waitlist)Returns
{ url, registrationId }so the frontend can redirect.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
stripeCheckoutSessionIdfield tosrc/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. Runpnpm typegenafter.Required env:
STRIPE_SECRET_KEY(already referenced),NEXT_PUBLIC_SITE_URL. Document both in the project README env section.Describe alternatives you've considered
beforeChangehook — 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
src/lib/stripe.ts:1src/payload/hooks/socialSessionRegistrations/checkCapacity.ts:5src/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.unit_amount.Before merging
pnpm typegen)