ghostguild-org/server/utils/helcim.js

150 lines
5.2 KiB
JavaScript

// 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).
import { randomBytes } from 'node:crypto'
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
const IDEMPOTENCY_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
/**
* Generic Helcim API fetcher.
*
* Reads the API token from runtime config on every call (do not hoist — Nitro
* runtime config is not guaranteed to be available at module import time).
* Throws `createError` with the upstream status on non-2xx responses. Network
* errors (fetch itself rejecting) propagate naturally so callers can decide
* how to handle them.
*
* @param {string} path - Path relative to the Helcim v2 base (must start with `/`).
* @param {object} [options]
* @param {string} [options.method='GET']
* @param {object} [options.body] - JSON-serializable request body.
* @param {string} [options.idempotencyKey] - Sent as `idempotency-key` header.
* @param {string} [options.errorMessage] - statusMessage used when the upstream returns non-OK.
* @returns {Promise<any>} Parsed JSON response.
*/
export async function helcimFetch(path, { method = 'GET', body, idempotencyKey, errorMessage } = {}) {
const helcimToken = useRuntimeConfig().helcimApiToken
if (!helcimToken) {
throw createError({
statusCode: 500,
statusMessage: 'Helcim API token not configured'
})
}
const headers = {
accept: 'application/json',
'api-token': helcimToken
}
if (body !== undefined) {
headers['content-type'] = 'application/json'
}
if (idempotencyKey) {
headers['idempotency-key'] = idempotencyKey
}
const response = await fetch(`${HELCIM_API_BASE}${path}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined
})
if (!response.ok) {
const errorText = await response.text()
console.error(`[helcim] ${method} ${path} ${response.status} ${errorText}`)
throw createError({
statusCode: response.status,
statusMessage: errorMessage || 'Helcim API error'
})
}
return await response.json()
}
// ---- 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',
body: { subscriptions: [subscription] },
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
}
/**
* Legacy stub — kept alive ONLY so `server/api/events/[id]/payment.post.js`
* still imports cleanly. The direct purchase API was never implemented.
* Always returns `{ success: false }`; callers surface the message to the user.
*/
export async function processHelcimPayment(_paymentData) {
return {
success: false,
message: 'Direct purchase API not implemented; use HelcimPay.js flow'
}
}