354 lines
10 KiB
JavaScript
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",
|
|
});
|
|
}
|
|
});
|