// Create a Helcim subscription import { getHelcimPlanId, requiresPayment } from '../../config/contributions.js' import Member from '../../models/member.js' import { connectDB } from '../../utils/mongoose.js' import { getSlackService } from '../../utils/slack.ts' import { requireAuth, getPaymentBridgeMember } from '../../utils/auth.js' import { createHelcimSubscription, generateIdempotencyKey, listHelcimCustomerTransactions } from '../../utils/helcim.js' import { sendWelcomeEmail } from '../../utils/resend.js' import { upsertPaymentFromHelcim } from '../../utils/payments.js' // Function to invite member to Slack async function inviteToSlack(member) { try { const slackService = getSlackService() if (!slackService) { console.warn('Slack service not configured, skipping invitation') return } console.log(`Processing Slack invitation for ${member.email}...`) const inviteResult = await slackService.inviteUserToSlack( member.email, member.name ) if (inviteResult.success) { const update = {} if (inviteResult.status === 'existing_user_added_to_channel' || inviteResult.status === 'user_already_in_channel' || inviteResult.status === 'new_user_invited_to_workspace') { update.slackInviteStatus = 'sent' update.slackUserId = inviteResult.userId update.slackInvited = true } else { update.slackInviteStatus = 'pending' update.slackInvited = false } await Member.findByIdAndUpdate( member._id, { $set: update }, { runValidators: false } ) // Send notification to vetting channel await slackService.notifyNewMember( member.name, member.email, member.circle, member.contributionAmount, inviteResult.status ) console.log(`Successfully processed Slack invitation for ${member.email}: ${inviteResult.status}`) } else { await Member.findByIdAndUpdate( member._id, { $set: { slackInviteStatus: 'failed' } }, { runValidators: false } ) console.error(`Failed to process Slack invitation for ${member.email}: ${inviteResult.error}`) // Don't throw error - subscription creation should still succeed } } catch (error) { console.error('Error during Slack invitation process:', error) try { await Member.findByIdAndUpdate( member._id, { $set: { slackInviteStatus: 'failed' } }, { runValidators: false } ) } catch (saveError) { console.error('Failed to update member Slack status:', saveError) } // Don't throw error - subscription creation should still succeed } } export default defineEventHandler(async (event) => { try { // Membership signup completes subscription before email verify; allow the // payment-bridge cookie set by /api/helcim/customer to satisfy auth here. const bridgeMember = await getPaymentBridgeMember(event) if (!bridgeMember) { await requireAuth(event) } await connectDB() const body = await validateBody(event, helcimSubscriptionSchema) // Check if payment is required if (!requiresPayment(body.contributionAmount)) { // For free tier, atomically capture pre-update status alongside the write. // Welcome email only fires on pending_payment → active transitions, not // on tier upgrades (active → active). const preMember = await Member.findOneAndUpdate( { helcimCustomerId: body.customerId }, { status: 'active', contributionAmount: body.contributionAmount, subscriptionStartDate: new Date() }, { new: false, projection: { status: 1 } } ) const isFirstActivation = preMember?.status === 'pending_payment' const member = await Member.findById(preMember._id) logActivity(member._id, 'subscription_created', { amount: body.contributionAmount }) await inviteToSlack(member) if (isFirstActivation) await sendWelcomeEmail(member) return { success: true, subscription: null, member: { id: member._id, email: member.email, name: member.name, circle: member.circle, contributionAmount: member.contributionAmount, status: member.status } } } // Validate card token is provided if (!body.cardToken) { throw createError({ statusCode: 400, statusMessage: 'Payment information is required for a paid contribution' }) } const cadence = body.cadence const paymentPlanId = getHelcimPlanId(cadence) const recurringAmount = cadence === 'annual' ? body.contributionAmount * 12 : body.contributionAmount if (!paymentPlanId) { throw createError({ statusCode: 500, statusMessage: cadence === 'annual' ? 'Annual plan id not configured' : 'Monthly plan id not configured', }) } const idempotencyKey = generateIdempotencyKey() const subscriptionPayload = { dateActivated: new Date().toISOString().split('T')[0], paymentPlanId: parseInt(paymentPlanId), customerCode: body.customerCode, recurringAmount, paymentMethod: 'card', } const subscriptionData = await createHelcimSubscription(subscriptionPayload, idempotencyKey) // Extract the first subscription from the response array const subscription = subscriptionData.data?.[0] if (!subscription) { throw createError({ statusCode: 500, statusMessage: 'Subscription creation failed' }) } const nextBillingDate = subscription.nextBillingDate ? new Date(subscription.nextBillingDate) : null // Atomically capture pre-update status alongside the write so we can // detect the pending_payment → active transition without a separate read // (which would race with concurrent webhooks/double-clicks). const preMember = await Member.findOneAndUpdate( { helcimCustomerId: body.customerId }, { $set: { contributionAmount: body.contributionAmount, helcimSubscriptionId: subscription.id, helcimCustomerId: body.customerId, paymentMethod: 'card', billingCadence: cadence, subscriptionStartDate: new Date(), status: 'active', ...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime()) ? { nextBillingDate } : {}), } }, { new: false, runValidators: false, projection: { status: 1 } } ) const isFirstActivation = preMember?.status === 'pending_payment' const member = await Member.findById(preMember._id) logActivity(member._id, 'subscription_created', { amount: body.contributionAmount }) try { const txs = await listHelcimCustomerTransactions(body.customerCode) const latestPaid = txs.find((t) => t.status === 'paid') if (latestPaid) { await upsertPaymentFromHelcim(member, latestPaid, { paymentType: cadence, sendConfirmation: true }) } } catch (err) { console.error('[payments] initial charge log failed, will be picked up by reconciliation:', err?.message || err) } await inviteToSlack(member) if (isFirstActivation) await sendWelcomeEmail(member) return { success: true, subscription: { subscriptionId: subscription.id, status: subscription.status, nextBillingDate: subscription.nextBillingDate }, member: { id: member._id, email: member.email, name: member.name, circle: member.circle, contributionAmount: member.contributionAmount, status: member.status } } } catch (error) { if (error.statusCode) throw error console.error('Error creating Helcim subscription:', error) throw createError({ statusCode: 500, statusMessage: 'An unexpected error occurred' }) } })