// Update member's contribution tier import { getHelcimPlanId, requiresPayment, } from "../../config/contributions.js"; import { connectDB } from "../../utils/mongoose.js"; import Member from "../../models/member.js"; import { getHelcimCustomer, listHelcimCustomerCards, createHelcimSubscription, updateHelcimSubscription, cancelHelcimSubscription, generateIdempotencyKey, } from "../../utils/helcim.js"; export default defineEventHandler(async (event) => { try { const member = await requireAuth(event); await connectDB(); const body = await validateBody(event, updateContributionSchema); const oldAmount = member.contributionAmount; const newAmount = body.contributionAmount; // If same amount, nothing to do if (oldAmount === newAmount) { return { success: true, message: "Already contributing this amount", }; } // Log contribution change (fire-and-forget, at the top so it logs regardless of which case path executes) const logContributionChange = () => { logActivity(member._id, 'contribution_changed', { from: oldAmount, to: newAmount }) } const oldRequiresPayment = requiresPayment(oldAmount); const newRequiresPayment = requiresPayment(newAmount); // Case 1: Moving from free to paid tier if (!oldRequiresPayment && newRequiresPayment) { // Check if member has Helcim customer ID with saved payment method if (!member.helcimCustomerId) { throw createError({ statusCode: 400, statusMessage: "Please use the subscription creation flow to upgrade to a paid tier", data: { requiresPaymentSetup: true }, }); } // Resolve plan id before entering the try/catch (so missing plan → 500, not swallowed 400) const cadence = body.cadence; // defaulted to 'monthly' by Zod const paymentPlanId = getHelcimPlanId(cadence); if (!paymentPlanId) { throw createError({ statusCode: 500, statusMessage: cadence === 'annual' ? 'Annual plan id not configured' : 'Monthly plan id not configured', }); } try { const customerData = await getHelcimCustomer(member.helcimCustomerId); const customerCode = customerData.customerCode; if (!customerCode) { throw new Error("No customer code found"); } // Check for saved cards const cards = await listHelcimCustomerCards(member.helcimCustomerId); const hasCards = Array.isArray(cards) && cards.length > 0; if (!hasCards) { throw new Error("No saved payment methods"); } const idempotencyKey = generateIdempotencyKey(); const subscriptionData = await createHelcimSubscription( { dateActivated: new Date().toISOString().split("T")[0], paymentPlanId: parseInt(paymentPlanId), customerCode, recurringAmount: cadence === 'annual' ? newAmount * 12 : newAmount, paymentMethod: "card", }, idempotencyKey, ); const subscription = subscriptionData.data?.[0]; if (!subscription) { throw new Error("No subscription returned in response"); } // Update member record await Member.findByIdAndUpdate( member._id, { $set: { contributionAmount: newAmount, helcimSubscriptionId: subscription.id, paymentMethod: "card", status: "active", billingCadence: cadence, } }, { runValidators: false } ); logContributionChange(); return { success: true, message: "Successfully upgraded to paid tier", subscription: { subscriptionId: subscription.id, status: subscription.status, nextBillingDate: subscription.nextBillingDate, }, }; } catch (error) { console.error("Error creating subscription with saved payment:", error); // If we can't use saved payment, require new payment setup throw createError({ statusCode: 400, statusMessage: "Payment information required. You'll be redirected to complete payment setup.", data: { requiresPaymentSetup: true }, }); } } // Case 2: Moving from paid to free tier (cancel subscription) if (oldRequiresPayment && !newRequiresPayment) { if (member.helcimSubscriptionId) { try { await cancelHelcimSubscription(member.helcimSubscriptionId); } catch (cancelError) { console.error("Error canceling Helcim subscription:", cancelError); // Continue anyway - we'll update the member record } } // Update member to free tier await Member.findByIdAndUpdate( member._id, { $set: { contributionAmount: newAmount, helcimSubscriptionId: null, paymentMethod: "none", billingCadence: "monthly" } }, { runValidators: false } ); logContributionChange() return { success: true, message: "Successfully downgraded to free tier", }; } // Case 3: Moving between paid tiers if (oldRequiresPayment && newRequiresPayment) { if (!member.helcimSubscriptionId) { throw createError({ statusCode: 400, statusMessage: "Payment information required. You'll be redirected to complete payment setup.", data: { requiresPaymentSetup: true }, }); } const memberCadence = member.billingCadence || 'monthly'; if (body.cadence && body.cadence !== memberCadence) { throw createError({ statusCode: 400, statusMessage: 'Cadence switch not supported on existing subscription', }); } try { const subscriptionData = await updateHelcimSubscription( member.helcimSubscriptionId, { recurringAmount: memberCadence === 'annual' ? newAmount * 12 : newAmount } ); await Member.findByIdAndUpdate( member._id, { $set: { contributionAmount: newAmount } }, { runValidators: false } ); logContributionChange(); return { success: true, message: 'Successfully updated contribution level', subscription: subscriptionData, }; } catch (updateError) { console.error('Error updating Helcim subscription:', updateError); throw createError({ statusCode: 500, statusMessage: 'Subscription update failed' }); } } // Case 4: Moving between free tiers (shouldn't happen but handle it) await Member.findByIdAndUpdate( member._id, { $set: { contributionAmount: newAmount } }, { runValidators: false } ); logContributionChange() return { success: true, message: "Successfully updated contribution level", }; } catch (error) { if (error.statusCode) throw error; console.error("Error updating contribution tier:", error); throw createError({ statusCode: 500, statusMessage: "An unexpected error occurred", }); } });