// Update member's contribution tier import jwt from "jsonwebtoken"; import { getHelcimPlanId, requiresPayment, isValidContributionValue, } from "../../config/contributions.js"; import Member from "../../models/member.js"; import { connectDB } from "../../utils/mongoose.js"; const HELCIM_API_BASE = "https://api.helcim.com/v2"; export default defineEventHandler(async (event) => { try { await connectDB(); const config = useRuntimeConfig(event); const body = await readBody(event); const token = getCookie(event, "auth-token"); if (!token) { throw createError({ statusCode: 401, statusMessage: "Not authenticated", }); } // Decode JWT token let decoded; try { decoded = jwt.verify(token, process.env.JWT_SECRET); } catch (err) { throw createError({ statusCode: 401, statusMessage: "Invalid or expired token", }); } // Validate contribution tier if ( !body.contributionTier || !isValidContributionValue(body.contributionTier) ) { throw createError({ statusCode: 400, statusMessage: "Invalid contribution tier", }); } // Get member const member = await Member.findById(decoded.memberId); if (!member) { throw createError({ statusCode: 404, statusMessage: "Member not found", }); } const oldTier = member.contributionTier; const newTier = body.contributionTier; // If same tier, nothing to do if (oldTier === newTier) { return { success: true, message: "Already on this tier", member, }; } const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN; const oldRequiresPayment = requiresPayment(oldTier); const newRequiresPayment = requiresPayment(newTier); // 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 }, }); } // Try to fetch customer info from Helcim to check for saved cards const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN; try { const customerResponse = await fetch( `${HELCIM_API_BASE}/customers/${member.helcimCustomerId}`, { method: "GET", headers: { accept: "application/json", "api-token": helcimToken, }, }, ); if (!customerResponse.ok) { throw new Error("Failed to fetch customer info"); } const customerData = await customerResponse.json(); const customerCode = customerData.customerCode; if (!customerCode) { throw new Error("No customer code found"); } // Check if customer has saved cards const cardsResponse = await fetch( `${HELCIM_API_BASE}/card-terminals?customerId=${member.helcimCustomerId}`, { method: "GET", headers: { accept: "application/json", "api-token": helcimToken, }, }, ); let hasCards = false; if (cardsResponse.ok) { const cardsData = await cardsResponse.json(); hasCards = cardsData.cards && cardsData.cards.length > 0; } if (!hasCards) { throw new Error("No saved payment methods"); } // Create new subscription with saved payment method const newPlanId = getHelcimPlanId(newTier); if (!newPlanId) { throw createError({ statusCode: 400, statusMessage: `Plan not configured for tier ${newTier}`, }); } // Generate idempotency key const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let idempotencyKey = ""; for (let i = 0; i < 25; i++) { idempotencyKey += chars.charAt( Math.floor(Math.random() * chars.length), ); } // Get tier amount const { getContributionTierByValue } = await import( "../../config/contributions.js" ); const tierInfo = getContributionTierByValue(newTier); const subscriptionResponse = await fetch( `${HELCIM_API_BASE}/subscriptions`, { method: "POST", headers: { accept: "application/json", "content-type": "application/json", "api-token": helcimToken, "idempotency-key": idempotencyKey, }, body: JSON.stringify({ subscriptions: [ { dateActivated: new Date().toISOString().split("T")[0], paymentPlanId: parseInt(newPlanId), customerCode: customerCode, recurringAmount: parseFloat(tierInfo.amount), paymentMethod: "card", }, ], }), }, ); if (!subscriptionResponse.ok) { const errorText = await subscriptionResponse.text(); console.error("Failed to create subscription:", errorText); throw new Error(`Failed to create subscription: ${errorText}`); } const subscriptionData = await subscriptionResponse.json(); const subscription = subscriptionData.data?.[0]; if (!subscription) { throw new Error("No subscription returned in response"); } // Update member record member.contributionTier = newTier; member.helcimSubscriptionId = subscription.id; member.paymentMethod = "card"; member.status = "active"; await member.save(); return { success: true, message: "Successfully upgraded to paid tier", member, 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 { // Cancel Helcim subscription const response = await fetch( `${HELCIM_API_BASE}/subscriptions/${member.helcimSubscriptionId}`, { method: "DELETE", headers: { accept: "application/json", "api-token": helcimToken, }, }, ); if (!response.ok) { console.error( "Failed to cancel Helcim subscription:", response.status, ); } } catch (error) { console.error("Error canceling Helcim subscription:", error); // Continue anyway - we'll update the member record } } // Update member to free tier member.contributionTier = newTier; member.helcimSubscriptionId = null; member.paymentMethod = "none"; await member.save(); return { success: true, message: "Successfully downgraded to free tier", member, }; } // Case 3: Moving between paid tiers if (oldRequiresPayment && newRequiresPayment) { const newPlanId = getHelcimPlanId(newTier); if (!newPlanId) { throw createError({ statusCode: 400, statusMessage: `Plan not configured for tier ${newTier}`, }); } if (!member.helcimSubscriptionId) { // No subscription exists - they need to go through payment flow throw createError({ statusCode: 400, statusMessage: "Payment information required. You'll be redirected to complete payment setup.", data: { requiresPaymentSetup: true }, }); } try { // Update subscription plan in Helcim const response = await fetch( `${HELCIM_API_BASE}/subscriptions/${member.helcimSubscriptionId}`, { method: "PATCH", headers: { accept: "application/json", "content-type": "application/json", "api-token": helcimToken, }, body: JSON.stringify({ paymentPlanId: parseInt(newPlanId), }), }, ); if (!response.ok) { const errorText = await response.text(); console.error( "Failed to update Helcim subscription:", response.status, errorText, ); throw new Error(`Failed to update subscription: ${errorText}`); } const subscriptionData = await response.json(); // Update member record member.contributionTier = newTier; await member.save(); return { success: true, message: "Successfully updated contribution level", member, subscription: subscriptionData, }; } catch (error) { console.error("Error updating Helcim subscription:", error); throw createError({ statusCode: 500, statusMessage: error.message || "Failed to update subscription", }); } } // Case 4: Moving between free tiers (shouldn't happen but handle it) member.contributionTier = newTier; await member.save(); return { success: true, message: "Successfully updated contribution level", member, }; } catch (error) { console.error("Error updating contribution tier:", error); throw createError({ statusCode: error.statusCode || 500, statusMessage: error.message || "Failed to update contribution tier", }); } });