ghostguild-org/server/api/members/update-contribution.post.js

354 lines
10 KiB
JavaScript

// 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",
});
}
});