refactor(members): use helcim helper + fix wrong card-lookup URL

This commit is contained in:
Jennie Robinson Faber 2026-04-08 22:03:42 +01:00
parent 03d6a66b84
commit 0d792c7c70
2 changed files with 33 additions and 155 deletions

View file

@ -1,12 +1,10 @@
// Cancel member subscription // Cancel member subscription
import Member from "../../models/member.js"; import Member from "../../models/member.js";
import { cancelHelcimSubscription } from "../../utils/helcim.js";
const HELCIM_API_BASE = "https://api.helcim.com/v2";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
const member = await requireAuth(event); const member = await requireAuth(event);
const config = useRuntimeConfig(event);
// If already on free tier, nothing to cancel // If already on free tier, nothing to cancel
if (member.contributionTier === "0" || !member.helcimSubscriptionId) { if (member.contributionTier === "0" || !member.helcimSubscriptionId) {
@ -18,32 +16,10 @@ export default defineEventHandler(async (event) => {
}; };
} }
const helcimToken = config.helcimApiToken;
try { try {
// Cancel Helcim subscription await cancelHelcimSubscription(member.helcimSubscriptionId);
const response = await fetch( } catch (cancelError) {
`${HELCIM_API_BASE}/subscriptions/${member.helcimSubscriptionId}`, console.error("Error canceling Helcim subscription:", cancelError);
{
method: "DELETE",
headers: {
accept: "application/json",
"api-token": helcimToken,
},
},
);
if (!response.ok) {
const errorText = await response.text();
console.error(
"Failed to cancel Helcim subscription:",
response.status,
errorText,
);
// Continue anyway - we'll update the member record
}
} catch (error) {
console.error("Error canceling Helcim subscription:", error);
// Continue anyway - we'll update the member record // Continue anyway - we'll update the member record
} }

View file

@ -2,17 +2,23 @@
import { import {
getHelcimPlanId, getHelcimPlanId,
requiresPayment, requiresPayment,
getContributionTierByValue,
} from "../../config/contributions.js"; } from "../../config/contributions.js";
import { connectDB } from "../../utils/mongoose.js"; import { connectDB } from "../../utils/mongoose.js";
import Member from "../../models/member.js"; import Member from "../../models/member.js";
import {
const HELCIM_API_BASE = "https://api.helcim.com/v2"; getHelcimCustomer,
listHelcimCustomerCards,
createHelcimSubscription,
updateHelcimSubscription,
cancelHelcimSubscription,
generateIdempotencyKey,
} from "../../utils/helcim.js";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
const member = await requireAuth(event); const member = await requireAuth(event);
await connectDB(); await connectDB();
const config = useRuntimeConfig(event);
const body = await validateBody(event, updateContributionSchema); const body = await validateBody(event, updateContributionSchema);
const oldTier = member.contributionTier; const oldTier = member.contributionTier;
@ -31,7 +37,6 @@ export default defineEventHandler(async (event) => {
logActivity(member._id, 'contribution_changed', { from: oldTier, to: newTier }) logActivity(member._id, 'contribution_changed', { from: oldTier, to: newTier })
} }
const helcimToken = config.helcimApiToken;
const oldRequiresPayment = requiresPayment(oldTier); const oldRequiresPayment = requiresPayment(oldTier);
const newRequiresPayment = requiresPayment(newTier); const newRequiresPayment = requiresPayment(newTier);
@ -47,49 +52,17 @@ export default defineEventHandler(async (event) => {
}); });
} }
// Try to fetch customer info from Helcim to check for saved cards
const helcimToken = config.helcimApiToken;
try { try {
const customerResponse = await fetch( const customerData = await getHelcimCustomer(member.helcimCustomerId);
`${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; const customerCode = customerData.customerCode;
if (!customerCode) { if (!customerCode) {
throw new Error("No customer code found"); throw new Error("No customer code found");
} }
// Check if customer has saved cards // Check for saved cards (FIX: use the correct endpoint)
const cardsResponse = await fetch( const cards = await listHelcimCustomerCards(member.helcimCustomerId);
`${HELCIM_API_BASE}/card-terminals?customerId=${member.helcimCustomerId}`, const hasCards = Array.isArray(cards) && cards.length > 0;
{
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) { if (!hasCards) {
throw new Error("No saved payment methods"); throw new Error("No saved payment methods");
@ -105,34 +78,12 @@ export default defineEventHandler(async (event) => {
}); });
} }
// Generate idempotency key const idempotencyKey = generateIdempotencyKey();
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let idempotencyKey = "";
for (let i = 0; i < 25; i++) {
idempotencyKey += chars.charAt(
Math.floor(Math.random() * chars.length),
);
}
// Get tier amount // Get tier amount
const { getContributionTierByValue } = await import(
"../../config/contributions.js"
);
const tierInfo = getContributionTierByValue(newTier); const tierInfo = getContributionTierByValue(newTier);
const subscriptionResponse = await fetch( const subscriptionData = await createHelcimSubscription(
`${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], dateActivated: new Date().toISOString().split("T")[0],
paymentPlanId: parseInt(newPlanId), paymentPlanId: parseInt(newPlanId),
@ -140,18 +91,9 @@ export default defineEventHandler(async (event) => {
recurringAmount: parseFloat(tierInfo.amount), recurringAmount: parseFloat(tierInfo.amount),
paymentMethod: "card", paymentMethod: "card",
}, },
], idempotencyKey,
}),
},
); );
if (!subscriptionResponse.ok) {
const errorText = await subscriptionResponse.text();
console.error("Failed to create subscription:", errorText);
throw new Error('Subscription creation failed');
}
const subscriptionData = await subscriptionResponse.json();
const subscription = subscriptionData.data?.[0]; const subscription = subscriptionData.data?.[0];
if (!subscription) { if (!subscription) {
@ -192,26 +134,9 @@ export default defineEventHandler(async (event) => {
if (oldRequiresPayment && !newRequiresPayment) { if (oldRequiresPayment && !newRequiresPayment) {
if (member.helcimSubscriptionId) { if (member.helcimSubscriptionId) {
try { try {
// Cancel Helcim subscription await cancelHelcimSubscription(member.helcimSubscriptionId);
const response = await fetch( } catch (cancelError) {
`${HELCIM_API_BASE}/subscriptions/${member.helcimSubscriptionId}`, console.error("Error canceling Helcim subscription:", cancelError);
{
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 // Continue anyway - we'll update the member record
} }
} }
@ -253,34 +178,11 @@ export default defineEventHandler(async (event) => {
} }
try { try {
// Update subscription plan in Helcim const subscriptionData = await updateHelcimSubscription(
const response = await fetch( member.helcimSubscriptionId,
`${HELCIM_API_BASE}/subscriptions/${member.helcimSubscriptionId}`, { paymentPlanId: parseInt(newPlanId) },
{
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('Subscription update failed');
}
const subscriptionData = await response.json();
// Update member record // Update member record
await Member.findByIdAndUpdate( await Member.findByIdAndUpdate(
member._id, member._id,
@ -295,8 +197,8 @@ export default defineEventHandler(async (event) => {
message: "Successfully updated contribution level", message: "Successfully updated contribution level",
subscription: subscriptionData, subscription: subscriptionData,
}; };
} catch (error) { } catch (updateError) {
console.error("Error updating Helcim subscription:", error); console.error("Error updating Helcim subscription:", updateError);
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: "Subscription update failed", statusMessage: "Subscription update failed",