diff --git a/server/api/helcim/subscription.post.js b/server/api/helcim/subscription.post.js index f7d214f..c4faba2 100644 --- a/server/api/helcim/subscription.post.js +++ b/server/api/helcim/subscription.post.js @@ -1,11 +1,10 @@ // Create a Helcim subscription -import { getHelcimPlanId, requiresPayment } from '../../config/contributions.js' +import { getHelcimPlanId, requiresPayment, getContributionTierByValue } 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' - -const HELCIM_API_BASE = 'https://api.helcim.com/v2' +import { createHelcimSubscription, generateIdempotencyKey } from '../../utils/helcim.js' // Function to invite member to Slack async function inviteToSlack(member) { @@ -75,7 +74,6 @@ export default defineEventHandler(async (event) => { try { await requireAuth(event) await connectDB() - const config = useRuntimeConfig(event) const body = await validateBody(event, helcimSubscriptionSchema) // Check if payment is required @@ -159,91 +157,21 @@ export default defineEventHandler(async (event) => { } // Try to create subscription in Helcim - const helcimToken = config.helcimApiToken - - // Generate a proper alphanumeric idempotency key (exactly 25 characters) - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - let idempotencyKey = '' - for (let i = 0; i < 25; i++) { - idempotencyKey += chars.charAt(Math.floor(Math.random() * chars.length)) - } - + const idempotencyKey = generateIdempotencyKey() + // Get contribution tier details to set recurring amount - const { getContributionTierByValue } = await import('../../config/contributions.js') const tierInfo = getContributionTierByValue(body.contributionTier) - - const requestBody = { - subscriptions: [{ - dateActivated: new Date().toISOString().split('T')[0], // Today in YYYY-MM-DD format - paymentPlanId: parseInt(planId), - customerCode: body.customerCode, - recurringAmount: parseFloat(tierInfo.amount), - paymentMethod: 'card' - }] + + const subscriptionPayload = { + dateActivated: new Date().toISOString().split('T')[0], // Today in YYYY-MM-DD format + paymentPlanId: parseInt(planId), + customerCode: body.customerCode, + recurringAmount: parseFloat(tierInfo.amount), + paymentMethod: 'card' } - const requestHeaders = { - 'accept': 'application/json', - 'content-type': 'application/json', - 'api-token': helcimToken, - 'idempotency-key': idempotencyKey - } - + try { - const subscriptionResponse = await fetch(`${HELCIM_API_BASE}/subscriptions`, { - method: 'POST', - headers: requestHeaders, - body: JSON.stringify(requestBody) - }) - - if (!subscriptionResponse.ok) { - const errorText = await subscriptionResponse.text() - console.error('Subscription creation failed:', subscriptionResponse.status) - - // If it's a validation error, let's try to get more info about available plans - if (subscriptionResponse.status === 400 || subscriptionResponse.status === 404) { - // Plan might not exist -- update member status and proceed - const member = await Member.findOneAndUpdate( - { helcimCustomerId: body.customerId }, - { - status: 'active', - contributionTier: body.contributionTier, - subscriptionStartDate: new Date(), - paymentMethod: 'card', - cardToken: body.cardToken, - notes: `Payment successful but subscription creation failed: ${errorText}` - }, - { new: true } - ) - - // Send Slack invitation even when subscription setup fails - await inviteToSlack(member) - - return { - success: true, - subscription: { - subscriptionId: 'manual-' + Date.now(), - status: 'needs_setup', - nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) - }, - member: { - id: member._id, - email: member.email, - name: member.name, - circle: member.circle, - contributionTier: member.contributionTier, - status: member.status - }, - warning: 'Payment successful but recurring subscription needs manual setup' - } - } - - throw createError({ - statusCode: subscriptionResponse.status, - statusMessage: 'Subscription creation failed' - }) - } - - const subscriptionData = await subscriptionResponse.json() + const subscriptionData = await createHelcimSubscription(subscriptionPayload, idempotencyKey) // Extract the first subscription from the response array const subscription = subscriptionData.data?.[0] @@ -254,7 +182,7 @@ export default defineEventHandler(async (event) => { // Update member in database const member = await Member.findOneAndUpdate( { helcimCustomerId: body.customerId }, - { + { status: 'active', contributionTier: body.contributionTier, helcimSubscriptionId: subscription.id, @@ -285,24 +213,30 @@ export default defineEventHandler(async (event) => { status: member.status } } - } catch (fetchError) { - console.error('Error during subscription creation:', fetchError) - + } catch (helcimError) { + // The helper throws createError on non-OK responses (statusCode = upstream HTTP status) + // and lets network errors propagate. We treat 400/404 from upstream AND any network + // error as the "manual setup needed" fallback. Re-throw other upstream errors (e.g. 5xx). + if (helcimError.statusCode && helcimError.statusCode !== 400 && helcimError.statusCode !== 404) { + throw helcimError + } + console.error('Error during subscription creation:', helcimError) + // Still mark member as active since payment was successful const member = await Member.findOneAndUpdate( { helcimCustomerId: body.customerId }, - { + { status: 'active', contributionTier: body.contributionTier, subscriptionStartDate: new Date(), paymentMethod: 'card', cardToken: body.cardToken, - notes: `Payment successful but subscription creation failed: ${fetchError.message}` + notes: `Payment successful but subscription creation failed: ${helcimError.message || 'unknown error'}` }, { new: true } ) - // Send Slack invitation even when subscription fetch fails + // Send Slack invitation even when subscription setup fails await inviteToSlack(member) return {