# Conflicts: # server/api/auth/member.get.js # server/api/members/update-contribution.post.js # tests/server/api/update-contribution.test.js
237 lines
7.4 KiB
JavaScript
237 lines
7.4 KiB
JavaScript
// 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");
|
|
}
|
|
|
|
const nextBillingDate = subscription.nextBillingDate
|
|
? new Date(subscription.nextBillingDate)
|
|
: null;
|
|
|
|
// Update member record
|
|
await Member.findByIdAndUpdate(
|
|
member._id,
|
|
{ $set: {
|
|
contributionAmount: newAmount,
|
|
helcimSubscriptionId: subscription.id,
|
|
paymentMethod: "card",
|
|
status: "active",
|
|
billingCadence: cadence,
|
|
...(nextBillingDate && !Number.isNaN(nextBillingDate.getTime())
|
|
? { nextBillingDate }
|
|
: {}),
|
|
} },
|
|
{ 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" },
|
|
$unset: { nextBillingDate: 1 },
|
|
},
|
|
{ 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",
|
|
});
|
|
}
|
|
});
|