ghostguild-org/server/api/invite/accept.post.js
Jennie Robinson Faber d4000c18cf fix(launch-flow): send welcome email on free /accept-invite activation
Free invite acceptance previously created a Member and signed them in
without sending the welcome email — pre-registrants got nothing as the
join confirmation. Wire sendWelcomeEmail into the free branch matching
the pattern in members/create.post.js.

Paid /accept-invite activations continue to receive the welcome email
via /api/helcim/subscription on the pending_payment → active transition,
so this only changes the free path.
2026-04-30 14:40:13 +01:00

131 lines
4.2 KiB
JavaScript

import jwt from 'jsonwebtoken'
import PreRegistration from '../../models/preRegistration.js'
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
import { setAuthCookie } from '../../utils/auth.js'
import { assignMemberNumber } from '../../utils/memberNumber.js'
import { createHelcimCustomer } from '../../utils/helcim.js'
import { sendWelcomeEmail } from '../../utils/resend.js'
export default defineEventHandler(async (event) => {
const body = await validateBody(event, inviteAcceptSchema)
const config = useRuntimeConfig(event)
await connectDB()
// Re-verify the token is still valid (not expired)
let decoded
try {
decoded = jwt.verify(body.token, config.jwtSecret)
} catch {
throw createError({ statusCode: 401, statusMessage: 'Invalid or expired invitation link' })
}
if (decoded.type !== 'prereg-invite' || decoded.preRegistrationId !== body.preRegistrationId) {
throw createError({ statusCode: 401, statusMessage: 'Invalid or expired invitation link' })
}
const preReg = await PreRegistration.findById(body.preRegistrationId)
if (!preReg) {
throw createError({ statusCode: 404, statusMessage: 'Pre-registration not found' })
}
if (preReg.status === 'accepted') {
throw createError({ statusCode: 400, statusMessage: 'This invitation has already been accepted' })
}
// Check no existing member with this email
const existingMember = await Member.findOne({ email: preReg.email })
if (existingMember) {
throw createError({ statusCode: 409, statusMessage: 'A member with this email already exists' })
}
// For paid invites, create the Helcim customer up front so we can store the ID
// on the Member at creation time. Done before Member.create so a Helcim failure
// doesn't leave us with an orphan Member doc.
let helcimCustomer = null
if (body.contributionAmount > 0) {
helcimCustomer = await createHelcimCustomer({
customerType: 'PERSON',
contactName: body.name,
email: preReg.email,
})
}
// Create the member
const member = await Member.create({
email: preReg.email,
name: body.name,
pronouns: body.pronouns || undefined,
location: body.location || undefined,
circle: body.circle,
contributionAmount: body.contributionAmount,
bio: body.motivation || undefined,
status: body.contributionAmount === 0 ? 'active' : 'pending_payment',
helcimCustomerId: helcimCustomer?.id,
helcimCustomerCode: helcimCustomer?.customerCode,
agreement: { acceptedAt: new Date() },
})
await assignMemberNumber(member._id)
// Update pre-registration
await PreRegistration.findByIdAndUpdate(preReg._id, {
$set: {
status: 'accepted',
acceptedAt: new Date(),
memberId: member._id,
}
})
logActivity(member._id, 'member_joined', {
source: 'pre-registration',
preRegistrationId: preReg._id,
})
// Issue session cookie so the member is authenticated for any follow-up calls
// (Helcim initialize-payment for paid flow, dashboard fetches for free flow).
setAuthCookie(event, member)
// For free tier, redirect to welcome
if (body.contributionAmount === 0) {
await autoFlagPreExistingSlackAccess(member)
try {
await sendWelcomeEmail(member)
logActivity(member._id, 'email_sent', {
emailType: 'welcome',
subject: 'Welcome to Ghost Guild'
})
} catch (emailError) {
console.error('Failed to send welcome email:', emailError)
}
return {
success: true,
requiresPayment: false,
redirectUrl: '/member/dashboard',
member: {
id: member._id,
email: member.email,
name: member.name,
circle: member.circle,
contributionAmount: member.contributionAmount,
status: member.status,
}
}
}
// For paid tiers, return member + Helcim customer so frontend can proceed to payment
return {
success: true,
requiresPayment: true,
customerId: helcimCustomer.id,
customerCode: helcimCustomer.customerCode,
member: {
id: member._id,
email: member.email,
name: member.name,
circle: member.circle,
contributionAmount: member.contributionAmount,
status: member.status,
}
}
})