Implement multi-step registration process: Add step indicators, error handling, and payment processing for membership registration. Enhance form validation and user feedback with success and error messages. Refactor state management for improved clarity and maintainability.

This commit is contained in:
Jennie Robinson Faber 2025-09-03 14:47:13 +01:00
parent a88aa62198
commit 2ca290d6e0
22 changed files with 1994 additions and 213 deletions

View file

@ -0,0 +1,43 @@
import jwt from 'jsonwebtoken'
import Member from '../../models/member.js'
import { connectDB } from '../../utils/mongoose.js'
export default defineEventHandler(async (event) => {
await connectDB()
const token = getCookie(event, 'auth-token')
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Not authenticated'
})
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const member = await Member.findById(decoded.memberId).select('-__v')
if (!member) {
throw createError({
statusCode: 404,
statusMessage: 'Member not found'
})
}
return {
id: member._id,
email: member.email,
name: member.name,
circle: member.circle,
contributionTier: member.contributionTier,
membershipLevel: `${member.circle}-${member.contributionTier}`
}
} catch (err) {
console.error('Token verification error:', err)
throw createError({
statusCode: 401,
statusMessage: 'Invalid or expired token'
})
}
})

View file

@ -0,0 +1,136 @@
// Create a Helcim customer
import jwt from 'jsonwebtoken'
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)
// Validate required fields
if (!body.name || !body.email) {
throw createError({
statusCode: 400,
statusMessage: 'Name and email are required'
})
}
// Check if member already exists
const existingMember = await Member.findOne({ email: body.email })
if (existingMember) {
throw createError({
statusCode: 409,
statusMessage: 'A member with this email already exists'
})
}
// Get token directly from environment if not in config
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
if (!helcimToken) {
throw createError({
statusCode: 500,
statusMessage: 'Helcim API token not configured'
})
}
// Debug: Log token (first few chars only)
console.log('Using Helcim token:', helcimToken.substring(0, 10) + '...')
// Test the connection first with native fetch
try {
const testResponse = await fetch('https://api.helcim.com/v2/connection-test', {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
if (!testResponse.ok) {
throw new Error(`HTTP ${testResponse.status}: ${testResponse.statusText}`)
}
const testData = await testResponse.json()
console.log('Connection test passed:', testData)
} catch (testError) {
console.error('Connection test failed:', testError)
throw createError({
statusCode: 401,
statusMessage: `Helcim API connection failed: ${testError.message}`
})
}
// Create customer in Helcim using native fetch
const customerResponse = await fetch(`${HELCIM_API_BASE}/customers`, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken
},
body: JSON.stringify({
customerType: 'PERSON',
contactName: body.name,
email: body.email
})
})
if (!customerResponse.ok) {
const errorText = await customerResponse.text()
console.error('Customer creation failed:', customerResponse.status, errorText)
throw createError({
statusCode: customerResponse.status,
statusMessage: `Failed to create customer: ${errorText}`
})
}
const customerData = await customerResponse.json()
// Create member in database
const member = await Member.create({
email: body.email,
name: body.name,
circle: body.circle,
contributionTier: body.contributionTier,
helcimCustomerId: customerData.id,
status: 'pending_payment'
})
// Generate JWT token for the session
const token = jwt.sign(
{
memberId: member._id,
email: body.email,
helcimCustomerId: customerData.id
},
config.jwtSecret,
{ expiresIn: '24h' }
)
return {
success: true,
customerId: customerData.id,
customerCode: customerData.customerCode,
token,
member: {
id: member._id,
email: member.email,
name: member.name,
circle: member.circle,
contributionTier: member.contributionTier,
status: member.status
}
}
} catch (error) {
console.error('Error creating Helcim customer:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to create customer'
})
}
})

View file

@ -0,0 +1,62 @@
// Initialize HelcimPay.js session
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
const body = await readBody(event)
// Debug log the request body
console.log('Initialize payment request body:', body)
// Validate required fields
if (!body.customerId) {
throw createError({
statusCode: 400,
statusMessage: 'Customer ID is required'
})
}
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
// Initialize HelcimPay.js session
const response = await fetch(`${HELCIM_API_BASE}/helcim-pay/initialize`, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken
},
body: JSON.stringify({
paymentType: 'verify', // For card verification
amount: 0, // Must be exactly 0 for verification
currency: 'CAD',
customerCode: body.customerCode,
paymentMethod: 'cc'
})
})
if (!response.ok) {
const errorText = await response.text()
console.error('HelcimPay initialization failed:', response.status, errorText)
throw createError({
statusCode: response.status,
statusMessage: `Failed to initialize payment: ${errorText}`
})
}
const paymentData = await response.json()
return {
success: true,
checkoutToken: paymentData.checkoutToken,
secretToken: paymentData.secretToken
}
} catch (error) {
console.error('Error initializing HelcimPay:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to initialize payment'
})
}
})

View file

@ -0,0 +1,45 @@
// Get Helcim payment plans
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
console.log('Fetching payment plans from Helcim...')
const response = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
if (!response.ok) {
console.error('Failed to fetch payment plans:', response.status, response.statusText)
const errorText = await response.text()
console.error('Response body:', errorText)
throw createError({
statusCode: response.status,
statusMessage: `Failed to fetch payment plans: ${errorText}`
})
}
const plansData = await response.json()
console.log('Payment plans retrieved:', JSON.stringify(plansData, null, 2))
return {
success: true,
plans: plansData
}
} catch (error) {
console.error('Error fetching Helcim payment plans:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to fetch payment plans'
})
}
})

View file

@ -0,0 +1,282 @@
// Create a Helcim subscription
import { getHelcimPlanId, requiresPayment } 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)
// Validate required fields
if (!body.customerId || !body.contributionTier) {
throw createError({
statusCode: 400,
statusMessage: 'Customer ID and contribution tier are required'
})
}
if (!body.customerCode) {
throw createError({
statusCode: 400,
statusMessage: 'Customer code is required for subscription creation'
})
}
console.log('Subscription request body:', body)
// Check if payment is required
if (!requiresPayment(body.contributionTier)) {
console.log('No payment required for tier:', body.contributionTier)
// For free tier, just update member status
const member = await Member.findOneAndUpdate(
{ helcimCustomerId: body.customerId },
{
status: 'active',
contributionTier: body.contributionTier,
subscriptionStartDate: new Date()
},
{ new: true }
)
console.log('Updated member for free tier:', member)
return {
success: true,
subscription: null,
member
}
}
console.log('Payment required for tier:', body.contributionTier)
// Get the Helcim plan ID
const planId = getHelcimPlanId(body.contributionTier)
console.log('Plan ID for tier:', planId)
// Validate card token is provided
if (!body.cardToken) {
throw createError({
statusCode: 400,
statusMessage: 'Payment information is required for this contribution tier'
})
}
// Check if we have a configured plan for this tier
if (!planId) {
console.log('No Helcim plan configured for tier:', body.contributionTier)
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 no Helcim plan configured for tier ${body.contributionTier}`
},
{ new: true }
)
return {
success: true,
subscription: {
subscriptionId: 'manual-' + Date.now(),
status: 'needs_plan_setup',
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
},
member,
warning: `Payment successful but recurring plan needs to be set up in Helcim for the ${body.contributionTier} tier`
}
}
// Try to create subscription in Helcim
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
console.log('Attempting to create Helcim subscription with plan ID:', planId)
// 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))
}
// 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 requestHeaders = {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken,
'idempotency-key': idempotencyKey
}
console.log('Subscription request body:', requestBody)
console.log('Request headers:', requestHeaders)
console.log('Request URL:', `${HELCIM_API_BASE}/subscriptions`)
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:')
console.error('Status:', subscriptionResponse.status)
console.error('Status Text:', subscriptionResponse.statusText)
console.error('Headers:', Object.fromEntries(subscriptionResponse.headers.entries()))
console.error('Response Body:', errorText)
console.error('Request was:', {
url: `${HELCIM_API_BASE}/subscriptions`,
method: 'POST',
body: requestBody,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken ? 'present' : 'missing'
}
})
// If it's a validation error, let's try to get more info about available plans
if (subscriptionResponse.status === 400 || subscriptionResponse.status === 404) {
console.log('Plan might not exist. Trying to get list of available payment plans...')
// Try to fetch available payment plans
try {
const plansResponse = await fetch(`${HELCIM_API_BASE}/payment-plans`, {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
if (plansResponse.ok) {
const plansData = await plansResponse.json()
console.log('Available payment plans:', JSON.stringify(plansData, null, 2))
} else {
console.log('Could not fetch payment plans:', plansResponse.status, plansResponse.statusText)
}
} catch (planError) {
console.log('Error fetching payment plans:', planError.message)
}
// For now, just update member status and let user know we need to configure plans
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 }
)
return {
success: true,
subscription: {
subscriptionId: 'manual-' + Date.now(),
status: 'needs_setup',
error: errorText,
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
},
member,
warning: 'Payment successful but recurring subscription needs manual setup'
}
}
throw createError({
statusCode: subscriptionResponse.status,
statusMessage: `Failed to create subscription: ${errorText}`
})
}
const subscriptionData = await subscriptionResponse.json()
console.log('Subscription created successfully:', subscriptionData)
// Extract the first subscription from the response array
const subscription = subscriptionData.data?.[0]
if (!subscription) {
throw new Error('No subscription returned in response')
}
// Update member in database
const member = await Member.findOneAndUpdate(
{ helcimCustomerId: body.customerId },
{
status: 'active',
contributionTier: body.contributionTier,
helcimSubscriptionId: subscription.id,
subscriptionStartDate: new Date(),
paymentMethod: 'card'
},
{ new: true }
)
return {
success: true,
subscription: {
subscriptionId: subscription.id,
status: subscription.status,
nextBillingDate: subscription.nextBillingDate
},
member
}
} catch (fetchError) {
console.error('Error during subscription creation:', fetchError)
// 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}`
},
{ new: true }
)
return {
success: true,
subscription: {
subscriptionId: 'manual-' + Date.now(),
status: 'needs_setup',
error: fetchError.message,
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
},
member,
warning: 'Payment successful but recurring subscription needs manual setup'
}
}
} catch (error) {
console.error('Error creating Helcim subscription:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to create subscription'
})
}
})

View file

@ -0,0 +1,45 @@
// Get existing Helcim subscriptions to understand the format
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
console.log('Fetching existing subscriptions from Helcim...')
const response = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
if (!response.ok) {
console.error('Failed to fetch subscriptions:', response.status, response.statusText)
const errorText = await response.text()
console.error('Response body:', errorText)
throw createError({
statusCode: response.status,
statusMessage: `Failed to fetch subscriptions: ${errorText}`
})
}
const subscriptionsData = await response.json()
console.log('Existing subscriptions:', JSON.stringify(subscriptionsData, null, 2))
return {
success: true,
subscriptions: subscriptionsData
}
} catch (error) {
console.error('Error fetching Helcim subscriptions:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to fetch subscriptions'
})
}
})

View file

@ -0,0 +1,46 @@
// Test Helcim API connection
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
// Log token info (safely)
const tokenInfo = {
hasToken: !!config.public.helcimToken,
tokenLength: config.public.helcimToken ? config.public.helcimToken.length : 0,
tokenPrefix: config.public.helcimToken ? config.public.helcimToken.substring(0, 10) : null
}
console.log('Helcim Token Info:', tokenInfo)
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
// Try connection test endpoint
const response = await $fetch(`${HELCIM_API_BASE}/connection-test`, {
method: 'GET',
headers: {
'accept': 'application/json',
'api-token': helcimToken
}
})
return {
success: true,
message: 'Helcim API connection successful',
tokenInfo,
connectionResponse: response
}
} catch (error) {
console.error('Helcim test error:', error)
return {
success: false,
message: error.message || 'Failed to connect to Helcim API',
statusCode: error.statusCode,
tokenInfo: {
hasToken: !!useRuntimeConfig().public.helcimToken,
tokenLength: useRuntimeConfig().public.helcimToken ? useRuntimeConfig().public.helcimToken.length : 0
}
}
}
})

View file

@ -0,0 +1,77 @@
// Test minimal subscription creation to understand required fields
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
// Generate a 25-character idempotency key
const idempotencyKey = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`.substring(0, 25)
// Test with minimal fields first
const testRequest1 = {
customerCode: 'CST1020', // Use a recent customer code
planId: 20162
}
console.log('Testing subscription with minimal fields:', testRequest1)
try {
const response1 = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken,
'idempotency-key': idempotencyKey + 'a'
},
body: JSON.stringify(testRequest1)
})
const result1 = await response1.text()
console.log('Test 1 - Status:', response1.status)
console.log('Test 1 - Response:', result1)
if (!response1.ok) {
// Try with paymentPlanId instead
const testRequest2 = {
customerCode: 'CST1020',
paymentPlanId: 20162
}
console.log('Testing subscription with paymentPlanId:', testRequest2)
const response2 = await fetch(`${HELCIM_API_BASE}/subscriptions`, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken,
'idempotency-key': idempotencyKey + 'b'
},
body: JSON.stringify(testRequest2)
})
const result2 = await response2.text()
console.log('Test 2 - Status:', response2.status)
console.log('Test 2 - Response:', result2)
}
} catch (error) {
console.error('Test error:', error)
}
return {
success: true,
message: 'Check server logs for test results'
}
} catch (error) {
console.error('Error in test endpoint:', error)
throw createError({
statusCode: 500,
statusMessage: error.message
})
}
})

View file

@ -0,0 +1,71 @@
// Update customer billing address
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
const body = await readBody(event)
// Validate required fields
if (!body.customerId || !body.billingAddress) {
throw createError({
statusCode: 400,
statusMessage: 'Customer ID and billing address are required'
})
}
const { billingAddress } = body
// Validate billing address fields
if (!billingAddress.street || !billingAddress.city || !billingAddress.country || !billingAddress.postalCode) {
throw createError({
statusCode: 400,
statusMessage: 'Complete billing address is required'
})
}
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
// Update customer billing address in Helcim
const response = await fetch(`${HELCIM_API_BASE}/customers/${body.customerId}`, {
method: 'PATCH',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-token': helcimToken
},
body: JSON.stringify({
billingAddress: {
name: billingAddress.name,
street1: billingAddress.street,
city: billingAddress.city,
province: billingAddress.province || billingAddress.state,
country: billingAddress.country,
postalCode: billingAddress.postalCode
}
})
})
if (!response.ok) {
const errorText = await response.text()
console.error('Billing address update failed:', response.status, errorText)
throw createError({
statusCode: response.status,
statusMessage: `Failed to update billing address: ${errorText}`
})
}
const customerData = await response.json()
return {
success: true,
customer: customerData
}
} catch (error) {
console.error('Error updating billing address:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to update billing address'
})
}
})

View file

@ -0,0 +1,38 @@
// Verify payment token from HelcimPay.js
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig(event)
const body = await readBody(event)
// Validate required fields
if (!body.cardToken || !body.customerId) {
throw createError({
statusCode: 400,
statusMessage: 'Card token and customer ID are required'
})
}
console.log('Payment verification request:', {
customerId: body.customerId,
cardToken: body.cardToken ? 'present' : 'missing'
})
// Since HelcimPay.js already verified the payment and we have the card token,
// we can just return success. The card is already associated with the customer.
console.log('Payment already verified through HelcimPay.js, returning success')
return {
success: true,
cardToken: body.cardToken,
message: 'Payment verified successfully through HelcimPay.js'
}
} catch (error) {
console.error('Error verifying payment:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.message || 'Failed to verify payment'
})
}
})

View file

@ -0,0 +1,111 @@
// Server-side contribution config
// Copy of the client-side config for server use
// Central configuration for Ghost Guild Contribution Levels and Helcim Plans
export const CONTRIBUTION_TIERS = {
FREE: {
value: '0',
amount: 0,
label: '$0 - I need support right now',
tier: 'free',
helcimPlanId: null, // No Helcim plan needed for free tier
features: [
'Access to basic resources',
'Community forum access'
]
},
SUPPORTER: {
value: '5',
amount: 5,
label: '$5 - I can contribute a little',
tier: 'supporter',
helcimPlanId: 20162,
features: [
'All Free Membership benefits',
'Priority community support',
'Early access to events'
]
},
MEMBER: {
value: '15',
amount: 15,
label: '$15 - I can sustain the community',
tier: 'member',
helcimPlanId: null, // TODO: Create $15/month plan in Helcim dashboard
features: [
'All Supporter benefits',
'Access to premium workshops',
'Monthly 1-on-1 sessions',
'Advanced resource library'
]
},
ADVOCATE: {
value: '30',
amount: 30,
label: '$30 - I can support others too',
tier: 'advocate',
helcimPlanId: null, // TODO: Create $30/month plan in Helcim dashboard
features: [
'All Member benefits',
'Weekly group mentoring',
'Access to exclusive events',
'Direct messaging with experts'
]
},
CHAMPION: {
value: '50',
amount: 50,
label: '$50 - I want to sponsor multiple members',
tier: 'champion',
helcimPlanId: null, // TODO: Create $50/month plan in Helcim dashboard
features: [
'All Advocate benefits',
'Personal mentoring sessions',
'VIP event access',
'Custom project support',
'Annual strategy session'
]
}
};
// Get all contribution options as an array (useful for forms)
export const getContributionOptions = () => {
return Object.values(CONTRIBUTION_TIERS);
};
// Get valid contribution values for validation
export const getValidContributionValues = () => {
return Object.values(CONTRIBUTION_TIERS).map(tier => tier.value);
};
// Get contribution tier by value
export const getContributionTierByValue = (value) => {
return Object.values(CONTRIBUTION_TIERS).find(tier => tier.value === value);
};
// Get Helcim plan ID for a contribution tier
export const getHelcimPlanId = (contributionValue) => {
const tier = getContributionTierByValue(contributionValue);
return tier?.helcimPlanId || null;
};
// Check if a contribution tier requires payment
export const requiresPayment = (contributionValue) => {
const tier = getContributionTierByValue(contributionValue);
return tier?.amount > 0;
};
// Check if a contribution value is valid
export const isValidContributionValue = (value) => {
return getValidContributionValues().includes(value);
};
// Get contribution tier by Helcim plan ID
export const getContributionTierByHelcimPlan = (helcimPlanId) => {
return Object.values(CONTRIBUTION_TIERS).find(tier => tier.helcimPlanId === helcimPlanId);
};
// Get paid tiers only (excluding free tier)
export const getPaidContributionTiers = () => {
return Object.values(CONTRIBUTION_TIERS).filter(tier => tier.amount > 0);
};

View file

@ -22,8 +22,21 @@ const memberSchema = new mongoose.Schema({
enum: getValidContributionValues(),
required: true
},
status: {
type: String,
enum: ['pending_payment', 'active', 'suspended', 'cancelled'],
default: 'pending_payment'
},
helcimCustomerId: String,
helcimSubscriptionId: String,
paymentMethod: {
type: String,
enum: ['card', 'bank', 'none'],
default: 'none'
},
subscriptionStartDate: Date,
subscriptionEndDate: Date,
nextBillingDate: Date,
slackInvited: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
lastLogin: Date