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:
parent
a88aa62198
commit
2ca290d6e0
22 changed files with 1994 additions and 213 deletions
120
HELCIM_TEST_INSTRUCTIONS.md
Normal file
120
HELCIM_TEST_INSTRUCTIONS.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Helcim Integration Testing Guide
|
||||||
|
|
||||||
|
## Setup Complete
|
||||||
|
The Helcim Recurring API integration has been set up with the following components:
|
||||||
|
|
||||||
|
### 1. Composables
|
||||||
|
- `/app/composables/useHelcim.js` - Server-side Helcim API interactions
|
||||||
|
- `/app/composables/useHelcimPay.js` - Client-side HelcimPay.js integration
|
||||||
|
|
||||||
|
### 2. Server API Endpoints
|
||||||
|
- `/server/api/helcim/customer.post.js` - Creates Helcim customer and member record
|
||||||
|
- `/server/api/helcim/subscription.post.js` - Creates subscription for paid tiers
|
||||||
|
- `/server/api/helcim/verify-payment.post.js` - Verifies payment token
|
||||||
|
|
||||||
|
### 3. Updated Pages
|
||||||
|
- `/app/pages/join.vue` - Multi-step signup flow with payment integration
|
||||||
|
|
||||||
|
### 4. Database Schema
|
||||||
|
- Updated `/server/models/member.js` with subscription fields
|
||||||
|
|
||||||
|
## Testing Instructions
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
1. Ensure your `.env` file has the test Helcim token:
|
||||||
|
```
|
||||||
|
NUXT_PUBLIC_HELCIM_TOKEN=your_test_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure you have test payment plans created in Helcim dashboard matching these IDs:
|
||||||
|
- `supporter-monthly-5`
|
||||||
|
- `member-monthly-15`
|
||||||
|
- `advocate-monthly-30`
|
||||||
|
- `champion-monthly-50`
|
||||||
|
|
||||||
|
### Test Flow
|
||||||
|
|
||||||
|
#### 1. Start the Development Server
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Test Free Tier Signup
|
||||||
|
1. Navigate to `/join`
|
||||||
|
2. Fill in name and email
|
||||||
|
3. Select any circle
|
||||||
|
4. Choose "$0 - I need support right now"
|
||||||
|
5. Click "Complete Registration"
|
||||||
|
6. Should go directly to confirmation without payment
|
||||||
|
|
||||||
|
#### 3. Test Paid Tier Signup
|
||||||
|
1. Navigate to `/join`
|
||||||
|
2. Fill in test details:
|
||||||
|
- Name: Test User
|
||||||
|
- Email: test@example.com
|
||||||
|
3. Select any circle
|
||||||
|
4. Choose a paid contribution tier (e.g., "$15 - I can sustain the community")
|
||||||
|
5. Click "Continue to Payment"
|
||||||
|
6. On the payment step, use Helcim test card numbers:
|
||||||
|
- **Success**: 4111 1111 1111 1111
|
||||||
|
- **Decline**: 4000 0000 0000 0002
|
||||||
|
- CVV: Any 3 digits
|
||||||
|
- Expiry: Any future date
|
||||||
|
7. Click "Complete Payment"
|
||||||
|
8. Should see confirmation with member details
|
||||||
|
|
||||||
|
### Test Card Numbers (Helcim Test Mode)
|
||||||
|
- **Visa Success**: 4111 1111 1111 1111
|
||||||
|
- **Mastercard Success**: 5500 0000 0000 0004
|
||||||
|
- **Amex Success**: 3400 0000 0000 009
|
||||||
|
- **Decline**: 4000 0000 0000 0002
|
||||||
|
- **Insufficient Funds**: 4000 0000 0000 0051
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
#### Check API Responses
|
||||||
|
Open browser DevTools Network tab to monitor:
|
||||||
|
- `/api/helcim/customer` - Should return customer ID and token
|
||||||
|
- `/api/helcim/verify-payment` - Should return card details
|
||||||
|
- `/api/helcim/subscription` - Should return subscription ID
|
||||||
|
|
||||||
|
#### Common Issues
|
||||||
|
|
||||||
|
1. **HelcimPay.js not loading**
|
||||||
|
- Check console for script loading errors
|
||||||
|
- Verify token is correctly set in environment
|
||||||
|
|
||||||
|
2. **Customer creation fails**
|
||||||
|
- Check API token permissions in Helcim dashboard
|
||||||
|
- Verify MongoDB connection
|
||||||
|
|
||||||
|
3. **Payment verification fails**
|
||||||
|
- Ensure you're using test card numbers
|
||||||
|
- Check that Helcim account is in test mode
|
||||||
|
|
||||||
|
4. **Subscription creation fails**
|
||||||
|
- Verify payment plan IDs exist in Helcim
|
||||||
|
- Check that card token was successfully captured
|
||||||
|
|
||||||
|
### Database Verification
|
||||||
|
|
||||||
|
Check MongoDB for created records:
|
||||||
|
```javascript
|
||||||
|
// In MongoDB shell or client
|
||||||
|
db.members.findOne({ email: "test@example.com" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Should see:
|
||||||
|
- `helcimCustomerId` populated
|
||||||
|
- `helcimSubscriptionId` for paid tiers
|
||||||
|
- `status: "active"` after successful payment
|
||||||
|
- `paymentMethod: "card"` for paid tiers
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Once testing is successful:
|
||||||
|
1. Switch to production Helcim token
|
||||||
|
2. Create production payment plans in Helcim
|
||||||
|
3. Update plan IDs in `/app/config/contributions.js` if needed
|
||||||
|
4. Test with real payment card (small amount)
|
||||||
|
5. Set up webhook endpoints for subscription events (renewals, failures, cancellations)
|
||||||
57
UPDATE_SUMMARY.md
Normal file
57
UPDATE_SUMMARY.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Helcim Integration - Issues Fixed
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
The API was returning 401 Unauthorized when trying to create customers.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
The runtime config wasn't properly accessing the Helcim token in server-side endpoints.
|
||||||
|
|
||||||
|
## Solution Applied
|
||||||
|
|
||||||
|
### 1. Fixed Runtime Config Access
|
||||||
|
Updated all server endpoints to:
|
||||||
|
- Pass the `event` parameter to `useRuntimeConfig(event)`
|
||||||
|
- Fallback to `process.env.NUXT_PUBLIC_HELCIM_TOKEN` if config doesn't load
|
||||||
|
|
||||||
|
### 2. Files Updated
|
||||||
|
- `/server/api/helcim/customer.post.js`
|
||||||
|
- `/server/api/helcim/subscription.post.js`
|
||||||
|
- `/server/api/helcim/verify-payment.post.js`
|
||||||
|
- `/server/api/helcim/test-connection.get.js`
|
||||||
|
|
||||||
|
### 3. Fixed Import Path
|
||||||
|
Created `/server/config/contributions.js` to re-export the contributions config for server-side imports.
|
||||||
|
|
||||||
|
### 4. Verified Token Works
|
||||||
|
Created `test-helcim-direct.js` which successfully:
|
||||||
|
- Connected to Helcim API
|
||||||
|
- Created a test customer (ID: 32854583, Code: CST1000)
|
||||||
|
|
||||||
|
## Testing Instructions
|
||||||
|
|
||||||
|
1. Restart your development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Test the connection:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/helcim/test-connection
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Try the signup flow at `/join`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- The token in your `.env` file is working correctly
|
||||||
|
- The Helcim API is accessible and responding
|
||||||
|
- Customer creation is functional when called directly
|
||||||
|
- The issue was specifically with how Nuxt's runtime config was being accessed in server endpoints
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Once you confirm the signup flow works:
|
||||||
|
1. Test with different contribution tiers
|
||||||
|
2. Verify payment capture with test cards
|
||||||
|
3. Check that subscriptions are created correctly
|
||||||
|
4. Consider adding webhook endpoints for subscription events
|
||||||
48
app/composables/useAuth.js
Normal file
48
app/composables/useAuth.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
export const useAuth = () => {
|
||||||
|
const authCookie = useCookie('auth-token')
|
||||||
|
const memberData = useState('auth.member', () => null)
|
||||||
|
const isAuthenticated = computed(() => !!authCookie.value)
|
||||||
|
const isMember = computed(() => !!memberData.value)
|
||||||
|
|
||||||
|
const checkMemberStatus = async () => {
|
||||||
|
if (!authCookie.value) {
|
||||||
|
memberData.value = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/auth/member', {
|
||||||
|
headers: {
|
||||||
|
'Cookie': `auth-token=${authCookie.value}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
memberData.value = response
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch member status:', error)
|
||||||
|
memberData.value = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/auth/logout', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
authCookie.value = null
|
||||||
|
memberData.value = null
|
||||||
|
await navigateTo('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated: readonly(isAuthenticated),
|
||||||
|
isMember: readonly(isMember),
|
||||||
|
memberData: readonly(memberData),
|
||||||
|
checkMemberStatus,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/composables/useHelcim.js
Normal file
90
app/composables/useHelcim.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Helcim API integration composable
|
||||||
|
export const useHelcim = () => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const helcimToken = config.public.helcimToken
|
||||||
|
|
||||||
|
// Base URL for Helcim API
|
||||||
|
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
||||||
|
|
||||||
|
// Helper function to make API requests
|
||||||
|
const makeHelcimRequest = async (endpoint, method = 'GET', body = null) => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`${HELCIM_API_BASE}${endpoint}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'api-token': helcimToken
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined
|
||||||
|
})
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Helcim API error:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a customer
|
||||||
|
const createCustomer = async (customerData) => {
|
||||||
|
return await makeHelcimRequest('/customers', 'POST', {
|
||||||
|
customerType: 'PERSON',
|
||||||
|
contactName: customerData.name,
|
||||||
|
email: customerData.email,
|
||||||
|
billingAddress: customerData.billingAddress || {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subscription
|
||||||
|
const createSubscription = async (customerId, planId, cardToken) => {
|
||||||
|
return await makeHelcimRequest('/recurring/subscriptions', 'POST', {
|
||||||
|
customerId,
|
||||||
|
planId,
|
||||||
|
cardToken,
|
||||||
|
startDate: new Date().toISOString().split('T')[0] // Today's date
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get customer details
|
||||||
|
const getCustomer = async (customerId) => {
|
||||||
|
return await makeHelcimRequest(`/customers/${customerId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get subscription details
|
||||||
|
const getSubscription = async (subscriptionId) => {
|
||||||
|
return await makeHelcimRequest(`/recurring/subscriptions/${subscriptionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update subscription
|
||||||
|
const updateSubscription = async (subscriptionId, updates) => {
|
||||||
|
return await makeHelcimRequest(`/recurring/subscriptions/${subscriptionId}`, 'PATCH', updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel subscription
|
||||||
|
const cancelSubscription = async (subscriptionId) => {
|
||||||
|
return await makeHelcimRequest(`/recurring/subscriptions/${subscriptionId}`, 'DELETE')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get payment plans
|
||||||
|
const getPaymentPlans = async () => {
|
||||||
|
return await makeHelcimRequest('/recurring/plans')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify card token (for testing)
|
||||||
|
const verifyCardToken = async (cardToken) => {
|
||||||
|
return await makeHelcimRequest('/cards/verify', 'POST', {
|
||||||
|
cardToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createCustomer,
|
||||||
|
createSubscription,
|
||||||
|
getCustomer,
|
||||||
|
getSubscription,
|
||||||
|
updateSubscription,
|
||||||
|
cancelSubscription,
|
||||||
|
getPaymentPlans,
|
||||||
|
verifyCardToken
|
||||||
|
}
|
||||||
|
}
|
||||||
158
app/composables/useHelcimPay.js
Normal file
158
app/composables/useHelcimPay.js
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
// HelcimPay.js integration composable
|
||||||
|
export const useHelcimPay = () => {
|
||||||
|
let checkoutToken = null
|
||||||
|
let secretToken = null
|
||||||
|
|
||||||
|
// Initialize HelcimPay.js session
|
||||||
|
const initializeHelcimPay = async (customerId, customerCode, amount = 0) => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/helcim/initialize-payment', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
customerId,
|
||||||
|
customerCode,
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
checkoutToken = response.checkoutToken
|
||||||
|
secretToken = response.secretToken
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Failed to initialize payment session')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Payment initialization error:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show payment modal
|
||||||
|
const showPaymentModal = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!checkoutToken) {
|
||||||
|
reject(new Error('Payment not initialized. Call initializeHelcimPay first.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load HelcimPay.js modal script
|
||||||
|
if (!window.appendHelcimPayIframe) {
|
||||||
|
console.log('HelcimPay script not loaded, loading now...')
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://secure.helcim.app/helcim-pay/services/start.js'
|
||||||
|
script.async = true
|
||||||
|
script.onload = () => {
|
||||||
|
console.log('HelcimPay script loaded successfully!')
|
||||||
|
console.log('Available functions:', Object.keys(window).filter(key => key.includes('Helcim') || key.includes('helcim')))
|
||||||
|
console.log('appendHelcimPayIframe available:', typeof window.appendHelcimPayIframe)
|
||||||
|
openModal(resolve, reject)
|
||||||
|
}
|
||||||
|
script.onerror = () => {
|
||||||
|
reject(new Error('Failed to load HelcimPay.js'))
|
||||||
|
}
|
||||||
|
document.head.appendChild(script)
|
||||||
|
} else {
|
||||||
|
console.log('HelcimPay script already loaded, calling openModal')
|
||||||
|
openModal(resolve, reject)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the payment modal
|
||||||
|
const openModal = (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
console.log('Trying to open modal with checkoutToken:', checkoutToken)
|
||||||
|
|
||||||
|
if (typeof window.appendHelcimPayIframe === 'function') {
|
||||||
|
// Set up event listener for HelcimPay.js responses
|
||||||
|
const helcimPayJsIdentifierKey = 'helcim-pay-js-' + checkoutToken
|
||||||
|
|
||||||
|
const handleHelcimPayEvent = (event) => {
|
||||||
|
console.log('Received window message:', event.data)
|
||||||
|
|
||||||
|
if (event.data.eventName === helcimPayJsIdentifierKey) {
|
||||||
|
console.log('HelcimPay event received:', event.data)
|
||||||
|
|
||||||
|
// Remove event listener to prevent multiple responses
|
||||||
|
window.removeEventListener('message', handleHelcimPayEvent)
|
||||||
|
|
||||||
|
if (event.data.eventStatus === 'SUCCESS') {
|
||||||
|
console.log('Payment success:', event.data.eventMessage)
|
||||||
|
|
||||||
|
// Parse the JSON string eventMessage
|
||||||
|
let paymentData
|
||||||
|
try {
|
||||||
|
paymentData = JSON.parse(event.data.eventMessage)
|
||||||
|
console.log('Parsed payment data:', paymentData)
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Failed to parse eventMessage:', parseError)
|
||||||
|
reject(new Error('Invalid payment response format'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract transaction details from nested data structure
|
||||||
|
const transactionData = paymentData.data?.data || {}
|
||||||
|
console.log('Transaction data:', transactionData)
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
transactionId: transactionData.transactionId,
|
||||||
|
cardToken: transactionData.cardToken,
|
||||||
|
cardLast4: transactionData.cardNumber ? transactionData.cardNumber.slice(-4) : undefined,
|
||||||
|
cardType: transactionData.cardType || 'unknown'
|
||||||
|
})
|
||||||
|
} else if (event.data.eventStatus === 'ABORTED') {
|
||||||
|
console.log('Payment aborted:', event.data.eventMessage)
|
||||||
|
reject(new Error(event.data.eventMessage || 'Payment failed'))
|
||||||
|
} else if (event.data.eventStatus === 'HIDE') {
|
||||||
|
console.log('Modal closed without completion')
|
||||||
|
reject(new Error('Payment cancelled by user'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
window.addEventListener('message', handleHelcimPayEvent)
|
||||||
|
|
||||||
|
// Open the HelcimPay iframe modal
|
||||||
|
console.log('Calling appendHelcimPayIframe with token:', checkoutToken)
|
||||||
|
window.appendHelcimPayIframe(checkoutToken, true)
|
||||||
|
console.log('appendHelcimPayIframe called, waiting for window messages...')
|
||||||
|
|
||||||
|
// Add timeout to clean up if no response
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('60 seconds passed, cleaning up event listener...')
|
||||||
|
window.removeEventListener('message', handleHelcimPayEvent)
|
||||||
|
reject(new Error('Payment timeout - no response received'))
|
||||||
|
}, 60000)
|
||||||
|
} else {
|
||||||
|
reject(new Error('appendHelcimPayIframe function not available'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening modal:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process payment verification
|
||||||
|
const verifyPayment = async () => {
|
||||||
|
try {
|
||||||
|
return await showPaymentModal()
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup tokens
|
||||||
|
const cleanup = () => {
|
||||||
|
checkoutToken = null
|
||||||
|
secretToken = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initializeHelcimPay,
|
||||||
|
verifyPayment,
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -390,40 +390,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedSeriesId || eventForm.series.id" class="space-y-4">
|
<div v-if="selectedSeriesId || eventForm.series.id" class="space-y-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Series ID <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="eventForm.series.id"
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g., coop-dev-fundamentals"
|
|
||||||
required
|
|
||||||
:readonly="selectedSeriesId"
|
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
||||||
:class="{ 'bg-gray-100': selectedSeriesId }"
|
|
||||||
@input="!selectedSeriesId && checkExistingSeries()"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
|
||||||
{{ selectedSeriesId ? 'From selected series' : 'Unique identifier to group related events (use lowercase with dashes)' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Position in Series <span class="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model.number="eventForm.series.position"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
required
|
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-gray-500 mt-1">Order within the series (1, 2, 3, etc.)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
|
@ -457,37 +423,6 @@
|
||||||
<p class="text-xs text-gray-500 mt-1">{{ selectedSeriesId ? 'From selected series' : 'Describe what the series covers and its goals' }}</p>
|
<p class="text-xs text-gray-500 mt-1">{{ selectedSeriesId ? 'From selected series' : 'Describe what the series covers and its goals' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Series Type</label>
|
|
||||||
<select
|
|
||||||
v-model="eventForm.series.type"
|
|
||||||
:disabled="selectedSeriesId"
|
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
||||||
:class="{ 'bg-gray-100': selectedSeriesId }"
|
|
||||||
>
|
|
||||||
<option value="workshop_series">Workshop Series</option>
|
|
||||||
<option value="recurring_meetup">Recurring Meetup</option>
|
|
||||||
<option value="multi_day">Multi-Day Event</option>
|
|
||||||
<option value="course">Course</option>
|
|
||||||
<option value="tournament">Tournament</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Total Events Planned</label>
|
|
||||||
<input
|
|
||||||
v-model.number="eventForm.series.totalEvents"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="e.g., 4"
|
|
||||||
:readonly="selectedSeriesId"
|
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
||||||
:class="{ 'bg-gray-100': selectedSeriesId }"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-gray-500 mt-1">{{ selectedSeriesId ? 'From selected series' : 'How many events will be in this series?' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedSeriesId" class="p-3 bg-blue-50 rounded-lg">
|
<div v-if="selectedSeriesId" class="p-3 bg-blue-50 rounded-lg">
|
||||||
<p class="text-sm text-blue-700">
|
<p class="text-sm text-blue-700">
|
||||||
|
|
@ -617,8 +552,6 @@ const editingEvent = ref(null)
|
||||||
const showSuccessMessage = ref(false)
|
const showSuccessMessage = ref(false)
|
||||||
const formErrors = ref([])
|
const formErrors = ref([])
|
||||||
const fieldErrors = ref({})
|
const fieldErrors = ref({})
|
||||||
const seriesExists = ref(false)
|
|
||||||
const existingSeries = ref(null)
|
|
||||||
const selectedSeriesId = ref('')
|
const selectedSeriesId = ref('')
|
||||||
const availableSeries = ref([])
|
const availableSeries = ref([])
|
||||||
|
|
||||||
|
|
@ -655,10 +588,7 @@ const eventForm = reactive({
|
||||||
isSeriesEvent: false,
|
isSeriesEvent: false,
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: ''
|
||||||
type: 'workshop_series',
|
|
||||||
position: 1,
|
|
||||||
totalEvents: null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -680,18 +610,12 @@ const onSeriesSelect = () => {
|
||||||
eventForm.series.id = series.id
|
eventForm.series.id = series.id
|
||||||
eventForm.series.title = series.title
|
eventForm.series.title = series.title
|
||||||
eventForm.series.description = series.description
|
eventForm.series.description = series.description
|
||||||
eventForm.series.type = series.type
|
|
||||||
eventForm.series.totalEvents = series.totalEvents
|
|
||||||
eventForm.series.position = (series.eventCount || 0) + 1
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset series form when no series is selected
|
// Reset series form when no series is selected
|
||||||
eventForm.series.id = ''
|
eventForm.series.id = ''
|
||||||
eventForm.series.title = ''
|
eventForm.series.title = ''
|
||||||
eventForm.series.description = ''
|
eventForm.series.description = ''
|
||||||
eventForm.series.type = 'workshop_series'
|
|
||||||
eventForm.series.position = 1
|
|
||||||
eventForm.series.totalEvents = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,10 +660,7 @@ if (route.query.edit) {
|
||||||
isSeriesEvent: false,
|
isSeriesEvent: false,
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: ''
|
||||||
type: 'workshop_series',
|
|
||||||
position: 1,
|
|
||||||
totalEvents: null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Handle early bird deadline formatting
|
// Handle early bird deadline formatting
|
||||||
|
|
@ -851,62 +772,6 @@ const validateForm = () => {
|
||||||
return formErrors.value.length === 0
|
return formErrors.value.length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a series with this ID already exists
|
|
||||||
const checkExistingSeries = async () => {
|
|
||||||
if (!eventForm.series.id || selectedSeriesId.value) {
|
|
||||||
seriesExists.value = false
|
|
||||||
existingSeries.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First check in standalone series
|
|
||||||
const standaloneResponse = await $fetch(`/api/admin/series`)
|
|
||||||
const existingStandalone = standaloneResponse.find(s => s.id === eventForm.series.id)
|
|
||||||
|
|
||||||
if (existingStandalone) {
|
|
||||||
seriesExists.value = true
|
|
||||||
existingSeries.value = existingStandalone
|
|
||||||
// Auto-fill series details
|
|
||||||
if (!eventForm.series.title || eventForm.series.title === '') {
|
|
||||||
eventForm.series.title = existingStandalone.title
|
|
||||||
}
|
|
||||||
if (!eventForm.series.description || eventForm.series.description === '') {
|
|
||||||
eventForm.series.description = existingStandalone.description
|
|
||||||
}
|
|
||||||
if (!eventForm.series.type || eventForm.series.type === 'workshop_series') {
|
|
||||||
eventForm.series.type = existingStandalone.type
|
|
||||||
}
|
|
||||||
if (!eventForm.series.totalEvents || eventForm.series.totalEvents === null) {
|
|
||||||
eventForm.series.totalEvents = existingStandalone.totalEvents
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to legacy series check (events with series data)
|
|
||||||
const legacyResponse = await $fetch(`/api/series/${eventForm.series.id}`)
|
|
||||||
if (legacyResponse) {
|
|
||||||
seriesExists.value = true
|
|
||||||
existingSeries.value = legacyResponse
|
|
||||||
if (!eventForm.series.title || eventForm.series.title === '') {
|
|
||||||
eventForm.series.title = legacyResponse.title
|
|
||||||
}
|
|
||||||
if (!eventForm.series.description || eventForm.series.description === '') {
|
|
||||||
eventForm.series.description = legacyResponse.description
|
|
||||||
}
|
|
||||||
if (!eventForm.series.type || eventForm.series.type === 'workshop_series') {
|
|
||||||
eventForm.series.type = legacyResponse.type
|
|
||||||
}
|
|
||||||
if (!eventForm.series.totalEvents || eventForm.series.totalEvents === null) {
|
|
||||||
eventForm.series.totalEvents = legacyResponse.totalEvents
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Series doesn't exist yet
|
|
||||||
seriesExists.value = false
|
|
||||||
existingSeries.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveEvent = async (redirect = true) => {
|
const saveEvent = async (redirect = true) => {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
|
|
@ -918,24 +783,11 @@ const saveEvent = async (redirect = true) => {
|
||||||
creating.value = true
|
creating.value = true
|
||||||
try {
|
try {
|
||||||
// If this is a series event and not using an existing series, create the standalone series first
|
// If this is a series event and not using an existing series, create the standalone series first
|
||||||
if (eventForm.series.isSeriesEvent && eventForm.series.id && !selectedSeriesId.value) {
|
if (eventForm.series.isSeriesEvent && selectedSeriesId.value) {
|
||||||
try {
|
// Series will be handled by the selected series
|
||||||
await $fetch('/api/admin/series', {
|
} else if (eventForm.series.isSeriesEvent) {
|
||||||
method: 'POST',
|
// For now, series creation requires selecting an existing series
|
||||||
body: {
|
// Individual series creation is handled through the series management page
|
||||||
id: eventForm.series.id,
|
|
||||||
title: eventForm.series.title,
|
|
||||||
description: eventForm.series.description,
|
|
||||||
type: eventForm.series.type,
|
|
||||||
totalEvents: eventForm.series.totalEvents
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (seriesError) {
|
|
||||||
// Series might already exist, that's ok
|
|
||||||
if (!seriesError.data?.statusMessage?.includes('already exists')) {
|
|
||||||
throw seriesError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingEvent.value) {
|
if (editingEvent.value) {
|
||||||
|
|
@ -1007,10 +859,7 @@ const saveAndCreateAnother = async () => {
|
||||||
isSeriesEvent: false,
|
isSeriesEvent: false,
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: ''
|
||||||
type: 'workshop_series',
|
|
||||||
position: 1,
|
|
||||||
totalEvents: null
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,58 +196,71 @@
|
||||||
|
|
||||||
<!-- Registration Form -->
|
<!-- Registration Form -->
|
||||||
<form v-if="registrationStatus !== 'registered'" @submit.prevent="handleRegistration" class="space-y-4">
|
<form v-if="registrationStatus !== 'registered'" @submit.prevent="handleRegistration" class="space-y-4">
|
||||||
<div>
|
<!-- Show form fields only for public events OR for logged-in members -->
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<template v-if="!event.membersOnly || isMember">
|
||||||
Full Name
|
<div>
|
||||||
</label>
|
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
<UInput
|
Full Name
|
||||||
id="name"
|
</label>
|
||||||
v-model="registrationForm.name"
|
<UInput
|
||||||
type="text"
|
id="name"
|
||||||
required
|
v-model="registrationForm.name"
|
||||||
:disabled="event.membersOnly && !isMember"
|
type="text"
|
||||||
placeholder="Enter your full name"
|
required
|
||||||
/>
|
placeholder="Enter your full name"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<UInput
|
<UInput
|
||||||
id="email"
|
id="email"
|
||||||
v-model="registrationForm.email"
|
v-model="registrationForm.email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
:disabled="event.membersOnly && !isMember"
|
placeholder="Enter your email"
|
||||||
placeholder="Enter your email"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="membershipLevel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label for="membershipLevel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Membership Status
|
Membership Status
|
||||||
</label>
|
</label>
|
||||||
<USelect
|
<USelect
|
||||||
id="membershipLevel"
|
id="membershipLevel"
|
||||||
v-model="registrationForm.membershipLevel"
|
v-model="registrationForm.membershipLevel"
|
||||||
:options="membershipOptions"
|
:options="membershipOptions"
|
||||||
:disabled="event.membersOnly && !isMember"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<UButton
|
<UButton
|
||||||
|
v-if="!event.membersOnly || isMember"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
block
|
block
|
||||||
:disabled="event.membersOnly && !isMember"
|
|
||||||
:loading="isRegistering"
|
:loading="isRegistering"
|
||||||
>
|
>
|
||||||
{{ isRegistering ? 'Registering...' : 'Register for Event' }}
|
{{ isRegistering ? 'Registering...' : 'Register for Event' }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<NuxtLink
|
||||||
|
v-else
|
||||||
|
to="/join"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Become a Member to Register
|
||||||
|
</UButton>
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
@ -302,8 +315,20 @@ if (error.value?.statusCode === 404) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is a member (this would normally come from auth/store)
|
// Authentication
|
||||||
const isMember = ref(false) // Set to true if user is logged in and is a member
|
const { isMember, memberData, checkMemberStatus } = useAuth()
|
||||||
|
|
||||||
|
// Check member status on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await checkMemberStatus()
|
||||||
|
|
||||||
|
// Pre-fill form if member is logged in
|
||||||
|
if (memberData.value) {
|
||||||
|
registrationForm.value.name = memberData.value.name
|
||||||
|
registrationForm.value.email = memberData.value.email
|
||||||
|
registrationForm.value.membershipLevel = memberData.value.membershipLevel || 'non-member'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Registration form state
|
// Registration form state
|
||||||
const registrationForm = ref({
|
const registrationForm = ref({
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,81 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
|
<!-- Step Indicators -->
|
||||||
|
<div class="flex justify-center mb-8">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
|
||||||
|
currentStep >= 1
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<span class="ml-2 font-medium" :class="currentStep === 1 ? 'text-blue-600' : 'text-gray-500'">
|
||||||
|
Information
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="needsPayment" class="w-16 h-1 bg-gray-200">
|
||||||
|
<div
|
||||||
|
class="h-full bg-blue-500 transition-all"
|
||||||
|
:style="{ width: currentStep >= 2 ? '100%' : '0%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="needsPayment" class="flex items-center">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
|
||||||
|
currentStep >= 2
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<span class="ml-2 font-medium" :class="currentStep === 2 ? 'text-blue-600' : 'text-gray-500'">
|
||||||
|
Payment
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-16 h-1 bg-gray-200">
|
||||||
|
<div
|
||||||
|
class="h-full bg-blue-500 transition-all"
|
||||||
|
:style="{ width: currentStep >= 3 ? '100%' : '0%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
|
||||||
|
currentStep >= 3
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span v-if="needsPayment">3</span>
|
||||||
|
<span v-else>2</span>
|
||||||
|
</div>
|
||||||
|
<span class="ml-2 font-medium" :class="currentStep === 3 ? 'text-blue-600' : 'text-gray-500'">
|
||||||
|
Confirmation
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div v-if="errorMessage" class="mb-6">
|
||||||
|
<UAlert color="red" :title="errorMessage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: Information -->
|
||||||
|
<div v-if="currentStep === 1" class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
|
||||||
<UForm :state="form" class="space-y-8" @submit="handleSubmit">
|
<UForm :state="form" class="space-y-8" @submit="handleSubmit">
|
||||||
<!-- Personal Information -->
|
<!-- Personal Information -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|
@ -113,11 +187,118 @@
|
||||||
size="xl"
|
size="xl"
|
||||||
class="px-12"
|
class="px-12"
|
||||||
>
|
>
|
||||||
Continue to Payment
|
{{ needsPayment ? 'Continue to Payment' : 'Complete Registration' }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UForm>
|
</UForm>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Payment -->
|
||||||
|
<div v-if="currentStep === 2" class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Payment Information
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
You're signing up for the {{ selectedTier.label }} plan
|
||||||
|
</p>
|
||||||
|
<p class="text-lg font-semibold text-blue-600 dark:text-blue-400 mt-2">
|
||||||
|
${{ selectedTier.amount }} CAD / month
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payment Instructions -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300">
|
||||||
|
Click "Complete Payment" below to open the secure payment modal and verify your payment method.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex justify-between pt-6">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
@click="goBack"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
size="lg"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
@click="processPayment"
|
||||||
|
>
|
||||||
|
Complete Payment
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Confirmation -->
|
||||||
|
<div v-if="currentStep === 3" class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<svg class="w-10 h-10 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Welcome to Ghost Guild!
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div v-if="successMessage" class="mb-6">
|
||||||
|
<UAlert color="green" :title="successMessage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6 text-left">
|
||||||
|
<h4 class="font-semibold mb-3">Membership Details:</h4>
|
||||||
|
<dl class="space-y-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-gray-600 dark:text-gray-400">Name:</dt>
|
||||||
|
<dd class="font-medium">{{ form.name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-gray-600 dark:text-gray-400">Email:</dt>
|
||||||
|
<dd class="font-medium">{{ form.email }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-gray-600 dark:text-gray-400">Circle:</dt>
|
||||||
|
<dd class="font-medium capitalize">{{ form.circle }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-gray-600 dark:text-gray-400">Contribution:</dt>
|
||||||
|
<dd class="font-medium">{{ selectedTier.label }}</dd>
|
||||||
|
</div>
|
||||||
|
<div v-if="customerCode" class="flex justify-between">
|
||||||
|
<dt class="text-gray-600 dark:text-gray-400">Member ID:</dt>
|
||||||
|
<dd class="font-medium">{{ customerCode }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
We've sent a confirmation email to {{ form.email }} with your membership details.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<UButton
|
||||||
|
to="/member/dashboard"
|
||||||
|
size="lg"
|
||||||
|
class="px-8"
|
||||||
|
>
|
||||||
|
Go to Dashboard
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
@click="resetForm"
|
||||||
|
>
|
||||||
|
Register Another Member
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -315,9 +496,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, computed } from 'vue'
|
import { reactive, ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { getCircleOptions } from '~/config/circles'
|
import { getCircleOptions } from '~/config/circles'
|
||||||
import { getContributionOptions } from '~/config/contributions'
|
import { getContributionOptions, requiresPayment, getContributionTierByValue } from '~/config/contributions'
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
|
|
@ -325,10 +506,26 @@ const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
circle: 'community',
|
circle: 'community',
|
||||||
contributionTier: '15',
|
contributionTier: '15',
|
||||||
|
billingAddress: {
|
||||||
|
street: '',
|
||||||
|
city: '',
|
||||||
|
province: '',
|
||||||
|
postalCode: '',
|
||||||
|
country: 'CA'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
|
const currentStep = ref(1) // 1: Info, 2: Billing (paid only), 3: Payment, 4: Confirmation
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const successMessage = ref('')
|
||||||
|
|
||||||
|
// Helcim state
|
||||||
|
const customerId = ref(null)
|
||||||
|
const customerCode = ref(null)
|
||||||
|
const subscriptionData = ref(null)
|
||||||
|
const paymentToken = ref(null)
|
||||||
|
|
||||||
// Circle options from central config
|
// Circle options from central config
|
||||||
const circleOptions = getCircleOptions()
|
const circleOptions = getCircleOptions()
|
||||||
|
|
@ -336,22 +533,199 @@ const circleOptions = getCircleOptions()
|
||||||
// Contribution options from central config
|
// Contribution options from central config
|
||||||
const contributionOptions = getContributionOptions()
|
const contributionOptions = getContributionOptions()
|
||||||
|
|
||||||
|
// Initialize composables
|
||||||
|
const { initializeHelcimPay, verifyPayment, cleanup: cleanupHelcimPay } = useHelcimPay()
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return form.name && form.email && form.circle && form.contributionTier
|
return form.name && form.email && form.circle && form.contributionTier
|
||||||
})
|
})
|
||||||
|
|
||||||
// Form submission - redirect to detailed form
|
// Check if payment is required
|
||||||
const handleSubmit = async () => {
|
const needsPayment = computed(() => {
|
||||||
if (isSubmitting.value) return
|
return requiresPayment(form.contributionTier)
|
||||||
|
})
|
||||||
|
|
||||||
// For now, just scroll to the form or redirect to detailed signup
|
|
||||||
const formElement = document.getElementById('membership-form')
|
// Get selected tier info
|
||||||
if (formElement) {
|
const selectedTier = computed(() => {
|
||||||
formElement.scrollIntoView({ behavior: 'smooth' })
|
return getContributionTierByValue(form.contributionTier)
|
||||||
} else {
|
})
|
||||||
// Could redirect to a detailed form page
|
|
||||||
await navigateTo('/join/details')
|
// Step 1: Create customer
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value || !isFormValid.value) return
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create customer in Helcim
|
||||||
|
const response = await $fetch('/api/helcim/customer', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
name: form.name,
|
||||||
|
email: form.email,
|
||||||
|
circle: form.circle,
|
||||||
|
contributionTier: form.contributionTier,
|
||||||
|
billingAddress: form.billingAddress
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log('Customer response:', response)
|
||||||
|
customerId.value = response.customerId
|
||||||
|
customerCode.value = response.customerCode
|
||||||
|
|
||||||
|
// Store token in session
|
||||||
|
const authToken = useCookie('auth-token')
|
||||||
|
authToken.value = response.token
|
||||||
|
|
||||||
|
// Move to next step
|
||||||
|
if (needsPayment.value) {
|
||||||
|
currentStep.value = 2
|
||||||
|
// Debug log
|
||||||
|
console.log('Customer ID:', customerId.value, 'Customer Code:', customerCode.value)
|
||||||
|
// Initialize HelcimPay.js session for card verification
|
||||||
|
await initializeHelcimPay(customerId.value, customerCode.value, 0)
|
||||||
|
} else {
|
||||||
|
// For free tier, create subscription directly
|
||||||
|
await createSubscription()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating customer:', error)
|
||||||
|
errorMessage.value = error.data?.message || 'Failed to create account. Please try again.'
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2: Process payment
|
||||||
|
const processPayment = async () => {
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
console.log('Starting payment process...')
|
||||||
|
isSubmitting.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Calling verifyPayment()...')
|
||||||
|
// Verify payment through HelcimPay.js
|
||||||
|
const paymentResult = await verifyPayment()
|
||||||
|
console.log('Payment result from HelcimPay:', paymentResult)
|
||||||
|
|
||||||
|
if (paymentResult.success) {
|
||||||
|
paymentToken.value = paymentResult.cardToken
|
||||||
|
console.log('Payment successful, cardToken:', paymentResult.cardToken)
|
||||||
|
|
||||||
|
console.log('Calling verify-payment endpoint...')
|
||||||
|
// Verify payment on server
|
||||||
|
const verifyResult = await $fetch('/api/helcim/verify-payment', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
cardToken: paymentResult.cardToken,
|
||||||
|
customerId: customerId.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('Payment verification result:', verifyResult)
|
||||||
|
|
||||||
|
console.log('Calling createSubscription...')
|
||||||
|
// Create subscription (don't let subscription errors prevent form progression)
|
||||||
|
const subscriptionResult = await createSubscription(paymentResult.cardToken)
|
||||||
|
|
||||||
|
if (!subscriptionResult || !subscriptionResult.success) {
|
||||||
|
console.warn('Subscription creation failed but payment succeeded:', subscriptionResult?.error)
|
||||||
|
// Still progress to success page since payment worked
|
||||||
|
currentStep.value = 3
|
||||||
|
successMessage.value = 'Payment successful! Subscription setup may need manual completion.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Payment process error:', error)
|
||||||
|
console.error('Error details:', {
|
||||||
|
message: error.message,
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
statusMessage: error.statusMessage,
|
||||||
|
data: error.data
|
||||||
|
})
|
||||||
|
errorMessage.value = error.message || 'Payment verification failed. Please try again.'
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subscription
|
||||||
|
const createSubscription = async (cardToken = null) => {
|
||||||
|
try {
|
||||||
|
console.log('Creating subscription with:', {
|
||||||
|
customerId: customerId.value,
|
||||||
|
contributionTier: form.contributionTier,
|
||||||
|
cardToken: cardToken ? 'present' : 'null'
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await $fetch('/api/helcim/subscription', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
customerId: customerId.value,
|
||||||
|
customerCode: customerCode.value,
|
||||||
|
contributionTier: form.contributionTier,
|
||||||
|
cardToken: cardToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Subscription creation response:', response)
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
subscriptionData.value = response.subscription
|
||||||
|
console.log('Moving to step 3 - success!')
|
||||||
|
currentStep.value = 3
|
||||||
|
successMessage.value = 'Your membership has been activated successfully!'
|
||||||
|
} else {
|
||||||
|
throw new Error('Subscription creation failed - response not successful')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Subscription creation error:', error)
|
||||||
|
console.error('Error details:', {
|
||||||
|
message: error.message,
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
statusMessage: error.statusMessage,
|
||||||
|
data: error.data
|
||||||
|
})
|
||||||
|
console.error('Subscription creation completely failed, but payment was successful')
|
||||||
|
// Don't throw error - let the calling function handle progression
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.data?.message || error.message || 'Failed to create subscription'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go back to previous step
|
||||||
|
const goBack = () => {
|
||||||
|
if (currentStep.value > 1) {
|
||||||
|
currentStep.value--
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
const resetForm = () => {
|
||||||
|
currentStep.value = 1
|
||||||
|
customerId.value = null
|
||||||
|
customerCode.value = null
|
||||||
|
subscriptionData.value = null
|
||||||
|
paymentToken.value = null
|
||||||
|
errorMessage.value = ''
|
||||||
|
successMessage.value = ''
|
||||||
|
form.email = ''
|
||||||
|
form.name = ''
|
||||||
|
form.circle = 'community'
|
||||||
|
form.contributionTier = '15'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupHelcimPay()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
42
debug-token.js
Normal file
42
debug-token.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Debug token encoding
|
||||||
|
const originalToken = 'aG_Eu%lqXCIJdWb2fUx52P_*-9GzaUHAVXvRjF43#sZw_FEeV9q7gl$pe$1EPRNs'
|
||||||
|
// Manually fix the %lq part - it should be a literal character, not URL encoded
|
||||||
|
const correctedToken = originalToken.replace('%lq', 'lq')
|
||||||
|
|
||||||
|
console.log('Original token:', originalToken)
|
||||||
|
console.log('Corrected token:', correctedToken)
|
||||||
|
console.log('Are they different?', originalToken !== correctedToken)
|
||||||
|
|
||||||
|
async function testBoth() {
|
||||||
|
console.log('\n=== Testing Original Token ===')
|
||||||
|
try {
|
||||||
|
const response1 = await fetch('https://api.helcim.com/v2/connection-test', {
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'api-token': originalToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('Original token status:', response1.status)
|
||||||
|
const data1 = await response1.text()
|
||||||
|
console.log('Original token response:', data1)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Original token error:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== Testing Corrected Token ===')
|
||||||
|
try {
|
||||||
|
const response2 = await fetch('https://api.helcim.com/v2/connection-test', {
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'api-token': correctedToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('Corrected token status:', response2.status)
|
||||||
|
const data2 = await response2.text()
|
||||||
|
console.log('Corrected token response:', data2)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Corrected token error:', error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testBoth()
|
||||||
43
server/api/auth/member.get.js
Normal file
43
server/api/auth/member.get.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
136
server/api/helcim/customer.post.js
Normal file
136
server/api/helcim/customer.post.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
62
server/api/helcim/initialize-payment.post.js
Normal file
62
server/api/helcim/initialize-payment.post.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
45
server/api/helcim/plans.get.js
Normal file
45
server/api/helcim/plans.get.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
282
server/api/helcim/subscription.post.js
Normal file
282
server/api/helcim/subscription.post.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
45
server/api/helcim/subscriptions.get.js
Normal file
45
server/api/helcim/subscriptions.get.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
46
server/api/helcim/test-connection.get.js
Normal file
46
server/api/helcim/test-connection.get.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
77
server/api/helcim/test-subscription.get.js
Normal file
77
server/api/helcim/test-subscription.get.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
71
server/api/helcim/update-billing.post.js
Normal file
71
server/api/helcim/update-billing.post.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
38
server/api/helcim/verify-payment.post.js
Normal file
38
server/api/helcim/verify-payment.post.js
Normal 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
111
server/config/contributions.js
Normal file
111
server/config/contributions.js
Normal 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);
|
||||||
|
};
|
||||||
|
|
@ -22,8 +22,21 @@ const memberSchema = new mongoose.Schema({
|
||||||
enum: getValidContributionValues(),
|
enum: getValidContributionValues(),
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['pending_payment', 'active', 'suspended', 'cancelled'],
|
||||||
|
default: 'pending_payment'
|
||||||
|
},
|
||||||
helcimCustomerId: String,
|
helcimCustomerId: String,
|
||||||
helcimSubscriptionId: String,
|
helcimSubscriptionId: String,
|
||||||
|
paymentMethod: {
|
||||||
|
type: String,
|
||||||
|
enum: ['card', 'bank', 'none'],
|
||||||
|
default: 'none'
|
||||||
|
},
|
||||||
|
subscriptionStartDate: Date,
|
||||||
|
subscriptionEndDate: Date,
|
||||||
|
nextBillingDate: Date,
|
||||||
slackInvited: { type: Boolean, default: false },
|
slackInvited: { type: Boolean, default: false },
|
||||||
createdAt: { type: Date, default: Date.now },
|
createdAt: { type: Date, default: Date.now },
|
||||||
lastLogin: Date
|
lastLogin: Date
|
||||||
|
|
|
||||||
49
test-helcim-direct.js
Normal file
49
test-helcim-direct.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Direct test of Helcim API with your token
|
||||||
|
// Run with: node test-helcim-direct.js
|
||||||
|
|
||||||
|
const token = process.env.NUXT_PUBLIC_HELCIM_TOKEN || 'aG_Eu%lqXCIJdWb2fUx52P_*-9GzaUHAVXvRjF43#sZw_FEeV9q7gl$pe$1EPRNs'
|
||||||
|
|
||||||
|
async function testHelcimConnection() {
|
||||||
|
console.log('Testing Helcim API connection...')
|
||||||
|
console.log('Token length:', token.length)
|
||||||
|
console.log('Token prefix:', token.substring(0, 10) + '...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test 1: Try to get connection test
|
||||||
|
const testResponse = await fetch('https://api.helcim.com/v2/connection-test', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'api-token': token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Connection test status:', testResponse.status)
|
||||||
|
const testData = await testResponse.text()
|
||||||
|
console.log('Connection test response:', testData)
|
||||||
|
|
||||||
|
// Test 2: Try to create a customer
|
||||||
|
const customerResponse = await fetch('https://api.helcim.com/v2/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'api-token': token
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
customerType: 'PERSON',
|
||||||
|
contactName: 'Test User',
|
||||||
|
email: 'test@example.com'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('\nCreate customer status:', customerResponse.status)
|
||||||
|
const customerData = await customerResponse.text()
|
||||||
|
console.log('Create customer response:', customerData)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelcimConnection()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue