Persist nextBillingDate on subscription create/update; unset on cancel or downgrade to free. Account page displays the cached date and lazily refreshes from Helcim when the cached value is within 24h of now (or missing).
217 lines
No EOL
6.8 KiB
JavaScript
217 lines
No EOL
6.8 KiB
JavaScript
// Create a Helcim subscription
|
|
import { getHelcimPlanId, requiresPayment, getContributionTierByValue, getTierAmount } from '../../config/contributions.js'
|
|
import Member from '../../models/member.js'
|
|
import { connectDB } from '../../utils/mongoose.js'
|
|
import { getSlackService } from '../../utils/slack.ts'
|
|
import { requireAuth } from '../../utils/auth.js'
|
|
import { createHelcimSubscription, generateIdempotencyKey } from '../../utils/helcim.js'
|
|
import { sendWelcomeEmail } from '../../utils/resend.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.contributionTier,
|
|
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 {
|
|
await requireAuth(event)
|
|
await connectDB()
|
|
const body = await validateBody(event, helcimSubscriptionSchema)
|
|
|
|
// Only send welcome email when a member transitions from pending_payment
|
|
// to active for the first time — not on tier upgrades (active → active).
|
|
const priorMember = await Member.findOne(
|
|
{ helcimCustomerId: body.customerId },
|
|
{ status: 1 }
|
|
)
|
|
const isFirstActivation = priorMember?.status === 'pending_payment'
|
|
|
|
// Check if payment is required
|
|
if (!requiresPayment(body.contributionTier)) {
|
|
// For free tier, just update member status
|
|
const member = await Member.findOneAndUpdate(
|
|
{ helcimCustomerId: body.customerId },
|
|
{
|
|
status: 'active',
|
|
contributionTier: body.contributionTier,
|
|
subscriptionStartDate: new Date()
|
|
},
|
|
{ new: true }
|
|
)
|
|
|
|
logActivity(member._id, 'subscription_created', { tier: body.contributionTier })
|
|
|
|
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,
|
|
contributionTier: member.contributionTier,
|
|
status: member.status
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate card token is provided
|
|
if (!body.cardToken) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Payment information is required for this contribution tier'
|
|
})
|
|
}
|
|
|
|
const tierInfo = getContributionTierByValue(body.contributionTier)
|
|
const cadence = body.cadence
|
|
const paymentPlanId = getHelcimPlanId(cadence)
|
|
|
|
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: getTierAmount(tierInfo, cadence),
|
|
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
|
|
|
|
// Update member in database
|
|
const member = await Member.findOneAndUpdate(
|
|
{ helcimCustomerId: body.customerId },
|
|
{ $set: {
|
|
contributionTier: body.contributionTier,
|
|
helcimSubscriptionId: subscription.id,
|
|
helcimCustomerId: body.customerId,
|
|
paymentMethod: 'card',
|
|
billingCadence: cadence,
|
|
subscriptionStartDate: new Date(),
|
|
status: 'active',
|
|
...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime())
|
|
? { nextBillingDate }
|
|
: {}),
|
|
} },
|
|
{ new: true, runValidators: false }
|
|
)
|
|
|
|
logActivity(member._id, 'subscription_created', { tier: body.contributionTier })
|
|
|
|
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,
|
|
contributionTier: member.contributionTier,
|
|
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'
|
|
})
|
|
}
|
|
}) |