refactor(helcim): introduce centralized helcim helper
This commit is contained in:
parent
f9be1f3f01
commit
783459106f
1 changed files with 145 additions and 135 deletions
|
|
@ -1,140 +1,150 @@
|
||||||
// Helcim Payment Integration Utilities
|
// Helcim API helper — centralized fetcher and typed wrappers for Helcim v2 endpoints.
|
||||||
|
// Auto-imported by Nitro under server/utils, but server API handlers in this codebase
|
||||||
|
// use explicit imports (matching the existing `../../utils/auth.js` pattern).
|
||||||
|
|
||||||
export const processHelcimPayment = async (paymentData) => {
|
import { randomBytes } from 'node:crypto'
|
||||||
const { amount, paymentToken, customerData } = paymentData;
|
|
||||||
|
|
||||||
// Check if Helcim is configured
|
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
||||||
const helcimAccountId = process.env.HELCIM_ACCOUNT_ID;
|
|
||||||
const helcimApiToken = process.env.HELCIM_API_TOKEN;
|
|
||||||
|
|
||||||
if (!helcimAccountId || !helcimApiToken) {
|
const IDEMPOTENCY_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||||
console.warn('Helcim not configured - skipping payment processing');
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Payment processing not configured',
|
|
||||||
testMode: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
/**
|
||||||
// In production, you would make API calls to Helcim here
|
* Generic Helcim API fetcher.
|
||||||
// Example structure:
|
*
|
||||||
const response = await fetch('https://api.helcim.com/v2/payment/purchase', {
|
* Reads the API token from runtime config on every call (do not hoist — Nitro
|
||||||
method: 'POST',
|
* runtime config is not guaranteed to be available at module import time).
|
||||||
headers: {
|
* Throws `createError` with the upstream status on non-2xx responses. Network
|
||||||
'Content-Type': 'application/json',
|
* errors (fetch itself rejecting) propagate naturally so callers can decide
|
||||||
'api-token': helcimApiToken,
|
* how to handle them.
|
||||||
'account-id': helcimAccountId
|
*
|
||||||
},
|
* @param {string} path - Path relative to the Helcim v2 base (must start with `/`).
|
||||||
body: JSON.stringify({
|
* @param {object} [options]
|
||||||
amount,
|
* @param {string} [options.method='GET']
|
||||||
currency: 'CAD',
|
* @param {object} [options.body] - JSON-serializable request body.
|
||||||
paymentToken,
|
* @param {string} [options.idempotencyKey] - Sent as `idempotency-key` header.
|
||||||
customerCode: customerData.email,
|
* @param {string} [options.errorMessage] - statusMessage used when the upstream returns non-OK.
|
||||||
contactName: customerData.name,
|
* @returns {Promise<any>} Parsed JSON response.
|
||||||
billingAddress: {
|
*/
|
||||||
contactName: customerData.name,
|
export async function helcimFetch(path, { method = 'GET', body, idempotencyKey, errorMessage } = {}) {
|
||||||
email: customerData.email
|
const helcimToken = useRuntimeConfig().helcimApiToken
|
||||||
}
|
|
||||||
|
if (!helcimToken) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Helcim API token not configured'
|
||||||
})
|
})
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: result.success || false,
|
|
||||||
transactionId: result.transactionId,
|
|
||||||
customerId: result.customerCode,
|
|
||||||
message: result.message
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Helcim payment error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error.message || 'Payment processing failed'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createHelcimSubscription = async (subscriptionData) => {
|
|
||||||
const { customerId, planId, amount } = subscriptionData;
|
|
||||||
|
|
||||||
const helcimAccountId = process.env.HELCIM_ACCOUNT_ID;
|
|
||||||
const helcimApiToken = process.env.HELCIM_API_TOKEN;
|
|
||||||
|
|
||||||
if (!helcimAccountId || !helcimApiToken) {
|
|
||||||
console.warn('Helcim not configured - skipping subscription creation');
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Subscription processing not configured',
|
|
||||||
testMode: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const headers = {
|
||||||
// Create recurring payment plan
|
accept: 'application/json',
|
||||||
const response = await fetch('https://api.helcim.com/v2/payment/plan', {
|
'api-token': helcimToken
|
||||||
method: 'POST',
|
}
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
if (body !== undefined) {
|
||||||
'api-token': helcimApiToken,
|
headers['content-type'] = 'application/json'
|
||||||
'account-id': helcimAccountId
|
}
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
if (idempotencyKey) {
|
||||||
customerCode: customerId,
|
headers['idempotency-key'] = idempotencyKey
|
||||||
planName: `Ghost Guild ${planId}`,
|
}
|
||||||
amount,
|
|
||||||
currency: 'CAD',
|
const response = await fetch(`${HELCIM_API_BASE}${path}`, {
|
||||||
frequency: 'MONTHLY',
|
method,
|
||||||
startDate: new Date().toISOString().split('T')[0]
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined
|
||||||
})
|
})
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
return {
|
console.error(`[helcim] ${method} ${path} ${response.status} ${errorText}`)
|
||||||
success: result.success || false,
|
throw createError({
|
||||||
subscriptionId: result.planId,
|
statusCode: response.status,
|
||||||
message: result.message
|
statusMessage: errorMessage || 'Helcim API error'
|
||||||
};
|
})
|
||||||
} catch (error) {
|
|
||||||
console.error('Helcim subscription error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error.message || 'Subscription creation failed'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelHelcimSubscription = async (subscriptionId) => {
|
|
||||||
const helcimApiToken = process.env.HELCIM_API_TOKEN;
|
|
||||||
|
|
||||||
if (!helcimApiToken) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Subscription management not configured'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return await response.json()
|
||||||
const response = await fetch(`https://api.helcim.com/v2/payment/plan/${subscriptionId}/cancel`, {
|
}
|
||||||
|
|
||||||
|
// ---- Customers ----
|
||||||
|
|
||||||
|
export const getHelcimCustomer = (id) =>
|
||||||
|
helcimFetch(`/customers/${id}`, { errorMessage: 'Customer lookup failed' })
|
||||||
|
|
||||||
|
export const findHelcimCustomerByEmail = (email) =>
|
||||||
|
helcimFetch(`/customers?search=${encodeURIComponent(email)}`, { errorMessage: 'Customer search failed' })
|
||||||
|
|
||||||
|
export const createHelcimCustomer = (payload) =>
|
||||||
|
helcimFetch('/customers', { method: 'POST', body: payload, errorMessage: 'Customer creation failed' })
|
||||||
|
|
||||||
|
export const updateHelcimCustomer = (id, payload) =>
|
||||||
|
helcimFetch(`/customers/${id}`, { method: 'PATCH', body: payload, errorMessage: 'Billing update failed' })
|
||||||
|
|
||||||
|
export const listHelcimCustomerCards = (id) =>
|
||||||
|
helcimFetch(`/customers/${id}/cards`, { errorMessage: 'Card lookup failed' })
|
||||||
|
|
||||||
|
// ---- Subscriptions ----
|
||||||
|
|
||||||
|
export const createHelcimSubscription = (subscription, idempotencyKey) =>
|
||||||
|
helcimFetch('/subscriptions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
body: { subscriptions: [subscription] },
|
||||||
'api-token': helcimApiToken
|
idempotencyKey,
|
||||||
|
errorMessage: 'Subscription creation failed'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cancelHelcimSubscription = (id) =>
|
||||||
|
helcimFetch(`/subscriptions/${id}`, { method: 'DELETE', errorMessage: 'Subscription cancellation failed' })
|
||||||
|
|
||||||
|
export const updateHelcimSubscription = (id, payload) =>
|
||||||
|
helcimFetch(`/subscriptions/${id}`, { method: 'PATCH', body: payload, errorMessage: 'Subscription update failed' })
|
||||||
|
|
||||||
|
// ---- Payment plans (admin) ----
|
||||||
|
|
||||||
|
export const listHelcimPlans = () =>
|
||||||
|
helcimFetch('/payment-plans', { errorMessage: 'Failed to fetch payment plans' })
|
||||||
|
|
||||||
|
export const createHelcimPlan = (payload) =>
|
||||||
|
helcimFetch('/payment-plans', { method: 'POST', body: payload, errorMessage: 'Payment plan creation failed' })
|
||||||
|
|
||||||
|
export const listHelcimSubscriptions = () =>
|
||||||
|
helcimFetch('/subscriptions', { errorMessage: 'Failed to fetch subscriptions' })
|
||||||
|
|
||||||
|
// ---- HelcimPay.js ----
|
||||||
|
|
||||||
|
export const initializeHelcimPaySession = (payload) =>
|
||||||
|
helcimFetch('/helcim-pay/initialize', {
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
errorMessage: 'Payment initialization failed'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a 25-character idempotency key using `crypto.randomBytes`.
|
||||||
|
*
|
||||||
|
* Each byte is mapped to the 62-character alphabet via modulo. The modulo
|
||||||
|
* introduces a slight bias (256 % 62 = 8), but entropy from `randomBytes` —
|
||||||
|
* not perfect uniformity — is what matters for an idempotency key.
|
||||||
|
*
|
||||||
|
* @returns {string} 25-character key.
|
||||||
|
*/
|
||||||
|
export function generateIdempotencyKey() {
|
||||||
|
const bytes = randomBytes(25)
|
||||||
|
let key = ''
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
key += IDEMPOTENCY_ALPHABET[bytes[i] % IDEMPOTENCY_ALPHABET.length]
|
||||||
}
|
}
|
||||||
});
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
/**
|
||||||
|
* Legacy stub — kept alive ONLY so `server/api/events/[id]/payment.post.js`
|
||||||
return {
|
* still imports cleanly. The direct purchase API was never implemented.
|
||||||
success: result.success || false,
|
* Always returns `{ success: false }`; callers surface the message to the user.
|
||||||
message: result.message
|
*/
|
||||||
};
|
export async function processHelcimPayment(_paymentData) {
|
||||||
} catch (error) {
|
|
||||||
console.error('Helcim cancellation error:', error);
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || 'Subscription cancellation failed'
|
message: 'Direct purchase API not implemented; use HelcimPay.js flow'
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue